home_view.dart 17 KB

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