splittunneling_view.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. import 'package:flutter/cupertino.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_screenutil/flutter_screenutil.dart';
  4. import 'package:get/get.dart';
  5. import 'package:nomo/app/base/base_view.dart';
  6. import 'package:nomo/app/constants/iconfont/iconfont.dart';
  7. import 'package:nomo/app/widgets/click_opacity.dart';
  8. import 'package:nomo/app/widgets/ix_app_bar.dart';
  9. import 'package:nomo/app/widgets/ix_image.dart';
  10. import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
  11. import '../controllers/splittunneling_controller.dart';
  12. import '../selectapp/controllers/splittunneling_selectapp_controller.dart';
  13. class SplittunnelingView extends BaseView<SplittunnelingController> {
  14. const SplittunnelingView({super.key});
  15. @override
  16. PreferredSizeWidget? get appBar => IXAppBar(title: 'Split Tunneling');
  17. @override
  18. Widget buildContent(BuildContext context) {
  19. return Column(
  20. children: [
  21. Expanded(
  22. child: SingleChildScrollView(
  23. padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
  24. child: Column(
  25. crossAxisAlignment: CrossAxisAlignment.start,
  26. children: [
  27. // 顶部提示框
  28. _buildAlertBox(),
  29. 10.verticalSpaceFromWidth,
  30. // 模式选择区域
  31. _buildModeSelection(),
  32. ],
  33. ),
  34. ),
  35. ),
  36. // 底部信息框
  37. SafeArea(child: _buildInfoBox()),
  38. ],
  39. );
  40. }
  41. /// 构建顶部提示框
  42. Widget _buildAlertBox() {
  43. return Container(
  44. padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
  45. decoration: BoxDecoration(
  46. color: Get.reactiveTheme.cardColor,
  47. borderRadius: BorderRadius.circular(12.r),
  48. ),
  49. child: Row(
  50. children: [
  51. Icon(IconFont.icon67, size: 20.w, color: const Color(0xFFFFB800)),
  52. SizedBox(width: 12.w),
  53. Expanded(
  54. child: Text(
  55. 'Only one mode can be active at a time.',
  56. style: TextStyle(
  57. fontSize: 12.sp,
  58. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  59. fontWeight: FontWeight.w500,
  60. ),
  61. ),
  62. ),
  63. ],
  64. ),
  65. );
  66. }
  67. /// 构建模式选择区域
  68. Widget _buildModeSelection() {
  69. return Column(
  70. children: [
  71. _buildModeCard(
  72. mode: SplitTunnelingMode.exclude,
  73. icon: IconFont.icon42,
  74. title: 'Exclude selected apps from VPN',
  75. description:
  76. 'Choose apps that will connect directly without using the VPN.',
  77. onTap: () {
  78. controller.selectMode(SplitTunnelingMode.exclude);
  79. // 刷新显示
  80. controller.refreshSelectedApps();
  81. },
  82. ),
  83. 10.verticalSpaceFromWidth,
  84. _buildModeCard(
  85. mode: SplitTunnelingMode.include,
  86. icon: IconFont.icon43,
  87. title: 'Use VPN for selected apps only',
  88. description:
  89. 'Choose apps that will use the VPN while others connect normally.',
  90. onTap: () {
  91. controller.selectMode(SplitTunnelingMode.include);
  92. // 刷新显示
  93. controller.refreshSelectedApps();
  94. },
  95. ),
  96. ],
  97. );
  98. }
  99. /// 构建模式卡片
  100. Widget _buildModeCard({
  101. required SplitTunnelingMode mode,
  102. required IconData icon,
  103. required String title,
  104. required String description,
  105. required VoidCallback onTap,
  106. }) {
  107. return Container(
  108. decoration: BoxDecoration(
  109. color: Get.reactiveTheme.highlightColor,
  110. borderRadius: BorderRadius.circular(12.r),
  111. ),
  112. child: Column(
  113. children: [
  114. ClickOpacity(
  115. onTap: onTap,
  116. child: Padding(
  117. padding: EdgeInsets.all(14.w),
  118. child: Row(
  119. crossAxisAlignment: CrossAxisAlignment.start,
  120. children: [
  121. // 图标
  122. Container(
  123. width: 30.w,
  124. height: 30.w,
  125. decoration: BoxDecoration(
  126. color: Get.reactiveTheme.shadowColor,
  127. borderRadius: BorderRadius.circular(8.r),
  128. ),
  129. child: Icon(icon, color: Colors.white, size: 20.w),
  130. ),
  131. 10.horizontalSpace,
  132. // 标题和描述
  133. Expanded(
  134. child: Column(
  135. crossAxisAlignment: CrossAxisAlignment.start,
  136. children: [
  137. Text(
  138. title,
  139. style: TextStyle(
  140. fontSize: 14.sp,
  141. height: 1.4,
  142. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  143. ),
  144. ),
  145. Text(
  146. description,
  147. style: TextStyle(
  148. fontSize: 12.sp,
  149. height: 1.6,
  150. color: Get.reactiveTheme.hintColor,
  151. ),
  152. ),
  153. ],
  154. ),
  155. ),
  156. 10.horizontalSpace,
  157. // Switch 开关
  158. Obx(() {
  159. final isSelected = controller.selectedMode.value == mode;
  160. return CupertinoSwitch(
  161. value: isSelected,
  162. onChanged: (value) {
  163. if (value) {
  164. controller.selectMode(mode);
  165. } else {
  166. controller.selectMode(SplitTunnelingMode.none);
  167. }
  168. controller.refreshSelectedApps();
  169. },
  170. activeTrackColor: Get.reactiveTheme.shadowColor,
  171. thumbColor: Colors.white,
  172. inactiveThumbColor: Colors.white,
  173. inactiveTrackColor: Colors.grey,
  174. );
  175. }),
  176. ],
  177. ),
  178. ),
  179. ),
  180. // 显示选中的应用
  181. Obx(() {
  182. final isSelected = controller.selectedMode.value == mode;
  183. if (isSelected) {
  184. final selectedApps = controller.getSelectedApps(mode);
  185. if (selectedApps.isNotEmpty) {
  186. return _buildSelectedAppsPreview(selectedApps);
  187. }
  188. }
  189. return const SizedBox.shrink();
  190. }),
  191. ],
  192. ),
  193. );
  194. }
  195. /// 构建选中应用预览
  196. Widget _buildSelectedAppsPreview(List<Map<String, dynamic>> selectedApps) {
  197. return AnimatedContainer(
  198. duration: const Duration(milliseconds: 300),
  199. curve: Curves.easeInOut,
  200. child: ClickOpacity(
  201. onTap: () {
  202. controller.toSelectAppPage(controller.selectedMode.value);
  203. },
  204. child: Container(
  205. height: 44.w,
  206. padding: EdgeInsets.symmetric(horizontal: 16.w),
  207. decoration: BoxDecoration(
  208. color: Get.reactiveTheme.cardColor,
  209. borderRadius: BorderRadius.only(
  210. bottomLeft: Radius.circular(12.r),
  211. bottomRight: Radius.circular(12.r),
  212. ),
  213. ),
  214. child: Row(
  215. children: [
  216. Text(
  217. 'Select apps',
  218. style: TextStyle(
  219. fontSize: 12.sp,
  220. color: Get.reactiveTheme.hintColor,
  221. fontWeight: FontWeight.w500,
  222. ),
  223. ),
  224. SizedBox(width: 8.w),
  225. Expanded(
  226. child: Row(
  227. mainAxisAlignment: MainAxisAlignment.end,
  228. children: [
  229. // 显示应用图标
  230. ...selectedApps
  231. .take(3)
  232. .toList()
  233. .asMap()
  234. .entries
  235. .map(
  236. (entry) => AnimatedContainer(
  237. duration: Duration(
  238. milliseconds: 200 + (entry.key * 100),
  239. ),
  240. curve: Curves.easeOut,
  241. margin: EdgeInsets.only(right: 6.w),
  242. child: SlideTransition(
  243. position:
  244. Tween<Offset>(
  245. begin: const Offset(0, 1),
  246. end: Offset.zero,
  247. ).animate(
  248. CurvedAnimation(
  249. parent: kAlwaysCompleteAnimation,
  250. curve: Curves.easeOut,
  251. ),
  252. ),
  253. child: FadeTransition(
  254. opacity: Tween<double>(begin: 0.0, end: 1.0)
  255. .animate(
  256. CurvedAnimation(
  257. parent: kAlwaysCompleteAnimation,
  258. curve: Curves.easeOut,
  259. ),
  260. ),
  261. child: ClipRRect(
  262. borderRadius: BorderRadius.circular(4.r),
  263. child: IXImage(
  264. key: ValueKey(
  265. 'preview_${entry.value['packageName']}',
  266. ),
  267. source: entry.value['icon'],
  268. width: 24.w,
  269. height: 24.w,
  270. sourceType: ImageSourceType.memory,
  271. ),
  272. ),
  273. ),
  274. ),
  275. ),
  276. ),
  277. // 如果有更多应用,显示箭头
  278. if (selectedApps.length > 3) ...[
  279. SizedBox(width: 4.w),
  280. AnimatedScale(
  281. scale: 1.0,
  282. duration: const Duration(milliseconds: 300),
  283. curve: Curves.easeOut,
  284. child: Icon(
  285. Icons.arrow_forward_ios,
  286. size: 12.w,
  287. color: Get.reactiveTheme.hintColor,
  288. ),
  289. ),
  290. ],
  291. ],
  292. ),
  293. ),
  294. AnimatedRotation(
  295. turns: 0.0,
  296. duration: const Duration(milliseconds: 200),
  297. child: Icon(
  298. Icons.keyboard_arrow_right,
  299. size: 20.w,
  300. color: Get.reactiveTheme.hintColor,
  301. ),
  302. ),
  303. ],
  304. ),
  305. ),
  306. ),
  307. );
  308. }
  309. /// 构建底部信息框
  310. Widget _buildInfoBox() {
  311. return Container(
  312. margin: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
  313. padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
  314. decoration: BoxDecoration(
  315. color: Get.reactiveTheme.canvasColor,
  316. borderRadius: BorderRadius.circular(12.r),
  317. ),
  318. child: Column(
  319. children: [
  320. Row(
  321. children: [
  322. Icon(
  323. IconFont.icon22,
  324. size: 20.w,
  325. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  326. ),
  327. 4.horizontalSpace,
  328. Text(
  329. 'Customize your VPN',
  330. style: TextStyle(
  331. fontSize: 14.sp,
  332. height: 1.6,
  333. fontWeight: FontWeight.w500,
  334. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  335. ),
  336. ),
  337. ],
  338. ),
  339. 10.verticalSpaceFromWidth,
  340. Text(
  341. 'Split tunneling lets you control which apps use the VPN connection and which connect directly. It helps you manage bandwidth and access local or foreign content without turning off the VPN.',
  342. style: TextStyle(
  343. fontSize: 12.sp,
  344. color: Get.reactiveTheme.hintColor,
  345. height: 1.6,
  346. ),
  347. ),
  348. ],
  349. ),
  350. );
  351. }
  352. }