home_view.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. import 'package:flutter/material.dart' hide ConnectionState;
  2. import 'package:flutter_screenutil/flutter_screenutil.dart';
  3. import 'package:get/get.dart';
  4. import '../../../../config/theme/theme_extensions/theme_extension.dart';
  5. import '../../../base/base_view.dart';
  6. import '../../../constants/assets.dart';
  7. import '../../../dialog/error_dialog.dart';
  8. import '../../../routes/app_pages.dart';
  9. import '../../../widgets/click_opacity.dart';
  10. import '../widgets/connection_button.dart';
  11. import '../controllers/home_controller.dart';
  12. import 'package:flutter_svg/flutter_svg.dart';
  13. import '../widgets/menu_list.dart';
  14. class HomeView extends BaseView<HomeController> {
  15. const HomeView({super.key});
  16. @override
  17. bool get safeArea => true;
  18. @override
  19. bool get isPopScope => true;
  20. @override
  21. Widget buildContent(BuildContext context) {
  22. return Container(
  23. width: double.maxFinite,
  24. margin: EdgeInsets.symmetric(horizontal: 14.w),
  25. child: SafeArea(
  26. child: Column(
  27. crossAxisAlignment: CrossAxisAlignment.start,
  28. children: [
  29. Row(
  30. mainAxisAlignment: MainAxisAlignment.end,
  31. children: [
  32. ClickOpacity(
  33. child: Padding(
  34. padding: EdgeInsets.all(10.w),
  35. child: Icon(Icons.settings, size: 20.w),
  36. ),
  37. onTap: () {
  38. Get.toNamed(Routes.SETTING);
  39. // ErrorDialog.show(
  40. // message:
  41. // "The VPN was disconnected unexpectedly. Would you like to reconnect now to stay protected?",
  42. // );
  43. },
  44. ),
  45. ],
  46. ),
  47. 50.verticalSpaceFromWidth,
  48. Text(
  49. "Active time",
  50. style: TextStyle(
  51. fontSize: 18.sp,
  52. height: 1.5,
  53. color: Get.reactiveTheme.hintColor,
  54. ),
  55. ),
  56. 2.verticalSpaceFromWidth,
  57. Text(
  58. "10:00:00",
  59. style: TextStyle(
  60. fontSize: 28.sp,
  61. height: 1.5,
  62. color: Get.reactiveTheme.primaryColor,
  63. ),
  64. ),
  65. 20.verticalSpaceFromWidth,
  66. // 位置选择按钮和最近位置(叠在一起的效果)
  67. Stack(
  68. children: [
  69. Container(
  70. alignment: Alignment.center,
  71. margin: EdgeInsets.only(top: 138.w),
  72. child: _buildConnectionButton(),
  73. ),
  74. _buildLocationStack(),
  75. ],
  76. ),
  77. Expanded(
  78. child: Column(
  79. mainAxisAlignment: MainAxisAlignment.end,
  80. children: [MenuList()],
  81. ),
  82. ),
  83. ],
  84. ),
  85. ),
  86. );
  87. }
  88. Widget _buildConnectionButton() {
  89. return Obx(
  90. () => ConnectionButton(
  91. state: controller.coreController.state,
  92. onTap: () {
  93. controller.coreController.handleConnection();
  94. },
  95. ),
  96. );
  97. }
  98. /// 构建位置堆叠效果(选中位置 + 最近位置)
  99. Widget _buildLocationStack() {
  100. return Obx(() {
  101. if (controller.selectedLocation == null) {
  102. return const SizedBox.shrink();
  103. }
  104. return Stack(
  105. children: [
  106. // 最近位置列表(背景层)
  107. if (controller.recentLocations.isNotEmpty)
  108. _buildRecentLocationsCard(),
  109. // 选中位置(前景层)
  110. _buildSelectedLocationCard(),
  111. ],
  112. );
  113. });
  114. }
  115. /// 构建选中位置卡片
  116. Widget _buildSelectedLocationCard() {
  117. return GestureDetector(
  118. onTap: () {
  119. // 打开位置选择弹窗
  120. // showLocationSelectionModal();
  121. Get.toNamed(Routes.NODE);
  122. },
  123. child: Obx(() {
  124. return Container(
  125. height: 56.w,
  126. width: double.maxFinite,
  127. padding: EdgeInsets.only(left: 16.w, right: 10.w),
  128. decoration: BoxDecoration(
  129. color: Get.reactiveTheme.highlightColor,
  130. borderRadius: BorderRadius.circular(12.r),
  131. ),
  132. child: Row(
  133. children: [
  134. // 国旗图标
  135. ClipRRect(
  136. borderRadius: BorderRadius.circular(4.r), // 设置圆角
  137. child: SvgPicture.asset(
  138. Assets.getCountryFlagImage(
  139. controller.selectedLocation!.code!,
  140. ),
  141. width: 32.w,
  142. height: 24.w,
  143. fit: BoxFit.cover,
  144. placeholderBuilder: (context) => Container(
  145. width: 32.w,
  146. height: 24.w,
  147. decoration: BoxDecoration(
  148. borderRadius: BorderRadius.circular(4.r),
  149. color: Colors.grey[200],
  150. ),
  151. alignment: Alignment.center,
  152. child: Icon(
  153. Icons.flag,
  154. size: 16.w,
  155. color: Colors.grey[400],
  156. ),
  157. ),
  158. ),
  159. ),
  160. 10.horizontalSpace,
  161. // 位置名称
  162. Expanded(
  163. child: Text(
  164. '${controller.selectedLocation!.code} - ${controller.selectedLocation!.name}',
  165. style: TextStyle(
  166. fontSize: 16.sp,
  167. height: 1.5,
  168. fontWeight: FontWeight.w500,
  169. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  170. ),
  171. ),
  172. ),
  173. // 箭头图标
  174. Icon(
  175. Icons.arrow_forward_ios,
  176. size: 16.w,
  177. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  178. ),
  179. ],
  180. ),
  181. );
  182. }),
  183. );
  184. }
  185. /// 构建最近位置卡片(支持展开/收缩)
  186. Widget _buildRecentLocationsCard() {
  187. return Obx(() {
  188. return Container(
  189. margin: EdgeInsets.symmetric(horizontal: 10.w),
  190. padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 56.w, bottom: 0),
  191. decoration: BoxDecoration(
  192. color: Get.reactiveTheme.cardColor,
  193. borderRadius: BorderRadius.circular(12.r),
  194. ),
  195. child: Column(
  196. children: [
  197. ClickOpacity(
  198. onTap: () {
  199. controller.isRecentLocationsExpanded =
  200. !controller.isRecentLocationsExpanded;
  201. },
  202. child: SizedBox(
  203. height: 44.w,
  204. child: Row(
  205. children: [
  206. Icon(
  207. Icons.access_time,
  208. size: 16.w,
  209. color: Get.reactiveTheme.hintColor,
  210. ),
  211. SizedBox(width: 4.w),
  212. Text(
  213. 'Recent',
  214. style: TextStyle(
  215. fontSize: 12.sp,
  216. height: 1.2,
  217. color: Get.reactiveTheme.hintColor,
  218. ),
  219. ),
  220. const Spacer(),
  221. // 最近三个节点的国旗图标(收缩状态)或箭头(展开状态)
  222. Obx(() {
  223. return AnimatedOpacity(
  224. opacity: controller.isRecentLocationsExpanded
  225. ? 0.0
  226. : 1.0,
  227. duration: const Duration(milliseconds: 300),
  228. child: IgnorePointer(
  229. ignoring: controller.isRecentLocationsExpanded,
  230. child: Row(
  231. mainAxisSize: MainAxisSize.min,
  232. children: [
  233. ...controller.recentLocations.take(3).map((
  234. location,
  235. ) {
  236. return Container(
  237. margin: EdgeInsets.only(right: 4.w),
  238. decoration: BoxDecoration(
  239. borderRadius: BorderRadius.circular(5.r),
  240. border: Border.all(
  241. color: Get
  242. .reactiveTheme
  243. .textTheme
  244. .bodyLarge!
  245. .color!,
  246. width: 0.7,
  247. ),
  248. ),
  249. child: ClipRRect(
  250. borderRadius: BorderRadius.circular(4.r),
  251. child: SvgPicture.asset(
  252. Assets.getCountryFlagImage(
  253. location.code!,
  254. ),
  255. width: 24.w,
  256. height: 16.w,
  257. fit: BoxFit.cover,
  258. placeholderBuilder: (context) =>
  259. Container(
  260. width: 24.w,
  261. height: 16.w,
  262. decoration: BoxDecoration(
  263. borderRadius:
  264. BorderRadius.circular(4.r),
  265. color: Colors.grey[200],
  266. ),
  267. alignment: Alignment.center,
  268. child: Icon(
  269. Icons.flag,
  270. size: 10.w,
  271. color: Colors.grey[400],
  272. ),
  273. ),
  274. ),
  275. ),
  276. );
  277. }),
  278. ],
  279. ),
  280. ),
  281. );
  282. }),
  283. Obx(() {
  284. return AnimatedRotation(
  285. turns: controller.isRecentLocationsExpanded
  286. ? 0.25
  287. : 0.0,
  288. duration: const Duration(milliseconds: 300),
  289. child: Icon(
  290. Icons.keyboard_arrow_right,
  291. size: 20.w,
  292. color: Get.reactiveTheme.hintColor,
  293. ),
  294. );
  295. }),
  296. ],
  297. ),
  298. ),
  299. ),
  300. // 最近位置列表(可折叠)
  301. Obx(() {
  302. return ClipRect(
  303. child: AnimatedAlign(
  304. duration: const Duration(milliseconds: 300),
  305. curve: Curves.easeInOut,
  306. heightFactor: controller.isRecentLocationsExpanded
  307. ? 1.0
  308. : 0.0,
  309. alignment: Alignment.topLeft,
  310. child: AnimatedOpacity(
  311. opacity: controller.isRecentLocationsExpanded ? 1.0 : 0.0,
  312. duration: const Duration(milliseconds: 300),
  313. child: Column(
  314. children: controller.recentLocations.map((location) {
  315. return ClickOpacity(
  316. onTap: () {
  317. controller.selectLocation(location);
  318. },
  319. child: Column(
  320. children: [
  321. Divider(
  322. height: 1,
  323. color: Get.reactiveTheme.dividerColor,
  324. ),
  325. Container(
  326. margin: EdgeInsets.symmetric(vertical: 12.h),
  327. child: Row(
  328. mainAxisAlignment: MainAxisAlignment.center,
  329. crossAxisAlignment: CrossAxisAlignment.center,
  330. children: [
  331. // 国旗图标
  332. ClipRRect(
  333. borderRadius: BorderRadius.circular(
  334. 4.r,
  335. ), // 设置圆角
  336. child: SvgPicture.asset(
  337. Assets.getCountryFlagImage(
  338. location.code!,
  339. ),
  340. width: 28.w,
  341. height: 21.w,
  342. fit: BoxFit.cover,
  343. placeholderBuilder: (context) =>
  344. Container(
  345. color: Colors.grey[200],
  346. child: Icon(
  347. Icons.flag,
  348. size: 12.w,
  349. color: Colors.grey[400],
  350. ),
  351. ),
  352. ),
  353. ),
  354. SizedBox(width: 10.w),
  355. // 位置信息
  356. Expanded(
  357. child: Text(
  358. '${location.code} - ${location.name}',
  359. style: TextStyle(
  360. fontSize: 14.sp,
  361. fontWeight: FontWeight.w500,
  362. color: Get.reactiveTheme.hintColor,
  363. ),
  364. ),
  365. ),
  366. ],
  367. ),
  368. ),
  369. ],
  370. ),
  371. );
  372. }).toList(),
  373. ),
  374. ),
  375. ),
  376. );
  377. }),
  378. ],
  379. ),
  380. );
  381. });
  382. }
  383. }