home_view.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  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. Get.toNamed(Routes.NODE);
  128. },
  129. child: Obx(() {
  130. return Container(
  131. height: 56.w,
  132. width: double.maxFinite,
  133. padding: EdgeInsets.only(left: 16.w, right: 10.w),
  134. decoration: BoxDecoration(
  135. color: Get.reactiveTheme.highlightColor,
  136. borderRadius: BorderRadius.circular(12.r),
  137. ),
  138. child: Row(
  139. children: [
  140. // 国旗图标
  141. ClipRRect(
  142. borderRadius: BorderRadius.circular(4.r), // 设置圆角
  143. child: SvgPicture.asset(
  144. Assets.getCountryFlagImage(
  145. controller.selectedLocation?.country ?? '',
  146. ),
  147. width: 32.w,
  148. height: 24.w,
  149. fit: BoxFit.cover,
  150. // placeholderBuilder: (context) => Container(
  151. // width: 32.w,
  152. // height: 24.w,
  153. // decoration: BoxDecoration(
  154. // borderRadius: BorderRadius.circular(4.r),
  155. // color: Colors.grey[200],
  156. // ),
  157. // alignment: Alignment.center,
  158. // child: Icon(
  159. // Icons.flag,
  160. // size: 16.w,
  161. // color: Colors.grey[400],
  162. // ),
  163. // ),
  164. ),
  165. ),
  166. 10.horizontalSpace,
  167. // 位置名称
  168. Expanded(
  169. child: Text(
  170. '${controller.selectedLocation?.code ?? ''} - ${controller.selectedLocation?.name ?? ''}',
  171. style: TextStyle(
  172. fontSize: 16.sp,
  173. height: 1.5,
  174. fontWeight: FontWeight.w500,
  175. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  176. ),
  177. ),
  178. ),
  179. // 箭头图标
  180. Icon(
  181. Icons.arrow_forward_ios,
  182. size: 16.w,
  183. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  184. ),
  185. ],
  186. ),
  187. );
  188. }),
  189. );
  190. }
  191. /// 构建最近位置卡片(支持展开/收缩)
  192. Widget _buildRecentLocationsCard() {
  193. return Obx(() {
  194. return Container(
  195. margin: EdgeInsets.symmetric(horizontal: 10.w),
  196. padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 56.w, bottom: 0),
  197. decoration: BoxDecoration(
  198. color: Get.reactiveTheme.cardColor,
  199. borderRadius: BorderRadius.circular(12.r),
  200. ),
  201. child: Column(
  202. children: [
  203. GestureDetector(
  204. behavior: HitTestBehavior.opaque,
  205. onTap: () {
  206. controller.isRecentLocationsExpanded =
  207. !controller.isRecentLocationsExpanded;
  208. },
  209. child: SizedBox(
  210. height: 44.w,
  211. child: Row(
  212. children: [
  213. Icon(
  214. Icons.access_time,
  215. size: 16.w,
  216. color: Get.reactiveTheme.hintColor,
  217. ),
  218. SizedBox(width: 4.w),
  219. Text(
  220. 'Recent',
  221. style: TextStyle(
  222. fontSize: 12.sp,
  223. height: 1.2,
  224. color: Get.reactiveTheme.hintColor,
  225. ),
  226. ),
  227. const Spacer(),
  228. // 最近三个节点的国旗图标(收缩状态)或箭头(展开状态)
  229. Obx(() {
  230. return AnimatedOpacity(
  231. opacity: controller.isRecentLocationsExpanded
  232. ? 0.0
  233. : 1.0,
  234. duration: const Duration(milliseconds: 300),
  235. child: IgnorePointer(
  236. ignoring: controller.isRecentLocationsExpanded,
  237. child: Row(
  238. mainAxisSize: MainAxisSize.min,
  239. children: [
  240. ...controller.recentLocations.take(3).map((
  241. location,
  242. ) {
  243. return Container(
  244. margin: EdgeInsets.only(right: 4.w),
  245. decoration: BoxDecoration(
  246. borderRadius: BorderRadius.circular(5.r),
  247. border: Border.all(
  248. color: Get.reactiveTheme.canvasColor,
  249. width: 0.4,
  250. ),
  251. ),
  252. child: ClipRRect(
  253. borderRadius: BorderRadius.circular(4.r),
  254. child: SvgPicture.asset(
  255. Assets.getCountryFlagImage(
  256. location.country ?? '',
  257. ),
  258. width: 24.w,
  259. height: 16.w,
  260. fit: BoxFit.cover,
  261. // placeholderBuilder: (context) =>
  262. // Container(
  263. // width: 24.w,
  264. // height: 16.w,
  265. // decoration: BoxDecoration(
  266. // borderRadius:
  267. // BorderRadius.circular(4.r),
  268. // color: Colors.grey[200],
  269. // ),
  270. // alignment: Alignment.center,
  271. // child: Icon(
  272. // Icons.flag,
  273. // size: 10.w,
  274. // color: Colors.grey[400],
  275. // ),
  276. // ),
  277. ),
  278. ),
  279. );
  280. }),
  281. ],
  282. ),
  283. ),
  284. );
  285. }),
  286. Obx(() {
  287. return AnimatedRotation(
  288. turns: controller.isRecentLocationsExpanded
  289. ? 0.25
  290. : 0.0,
  291. duration: const Duration(milliseconds: 300),
  292. child: Icon(
  293. Icons.keyboard_arrow_right,
  294. size: 20.w,
  295. color: Get.reactiveTheme.hintColor,
  296. ),
  297. );
  298. }),
  299. ],
  300. ),
  301. ),
  302. ),
  303. // 最近位置列表(可折叠)
  304. Obx(() {
  305. return ClipRect(
  306. child: AnimatedAlign(
  307. duration: const Duration(milliseconds: 300),
  308. curve: Curves.easeInOut,
  309. heightFactor: controller.isRecentLocationsExpanded
  310. ? 1.0
  311. : 0.0,
  312. alignment: Alignment.topLeft,
  313. child: AnimatedOpacity(
  314. opacity: controller.isRecentLocationsExpanded ? 1.0 : 0.0,
  315. duration: const Duration(milliseconds: 300),
  316. child: Column(
  317. children: controller.recentLocations.map((location) {
  318. return ClickOpacity(
  319. onTap: () {
  320. controller.selectLocation(location);
  321. },
  322. child: Column(
  323. children: [
  324. Divider(
  325. height: 1,
  326. color: Get.reactiveTheme.dividerColor,
  327. ),
  328. Container(
  329. margin: EdgeInsets.symmetric(vertical: 12.h),
  330. child: Row(
  331. mainAxisAlignment: MainAxisAlignment.center,
  332. crossAxisAlignment: CrossAxisAlignment.center,
  333. children: [
  334. // 国旗图标
  335. ClipRRect(
  336. borderRadius: BorderRadius.circular(
  337. 4.r,
  338. ), // 设置圆角
  339. child: SvgPicture.asset(
  340. Assets.getCountryFlagImage(
  341. location.country ?? '',
  342. ),
  343. width: 28.w,
  344. height: 21.w,
  345. fit: BoxFit.cover,
  346. // placeholderBuilder: (context) =>
  347. // Container(
  348. // color: Colors.grey[200],
  349. // child: Icon(
  350. // Icons.flag,
  351. // size: 12.w,
  352. // color: Colors.grey[400],
  353. // ),
  354. // ),
  355. ),
  356. ),
  357. SizedBox(width: 10.w),
  358. // 位置信息
  359. Expanded(
  360. child: Text(
  361. '${location.code} - ${location.name}',
  362. style: TextStyle(
  363. fontSize: 14.sp,
  364. fontWeight: FontWeight.w500,
  365. color: Get.reactiveTheme.hintColor,
  366. ),
  367. ),
  368. ),
  369. ],
  370. ),
  371. ),
  372. ],
  373. ),
  374. );
  375. }).toList(),
  376. ),
  377. ),
  378. ),
  379. );
  380. }),
  381. ],
  382. ),
  383. );
  384. });
  385. }
  386. }