home_view.dart 14 KB

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