home_view.dart 15 KB

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