home_view.dart 17 KB

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