subscription_view.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_screenutil/flutter_screenutil.dart';
  3. import 'package:get/get.dart';
  4. import 'package:nomo/app/constants/iconfont/iconfont.dart';
  5. import 'package:nomo/config/theme/dark_theme_colors.dart';
  6. import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
  7. import 'package:video_player/video_player.dart';
  8. import '../../../constants/assets.dart';
  9. import '../../../widgets/info_card.dart';
  10. import '../../../widgets/ix_image.dart';
  11. import '../controllers/subscription_controller.dart';
  12. class SubscriptionView extends GetView<SubscriptionController> {
  13. const SubscriptionView({super.key});
  14. @override
  15. Widget build(BuildContext context) {
  16. return Scaffold(
  17. backgroundColor: DarkThemeColors.scaffoldBackgroundColor,
  18. body: Stack(
  19. children: [
  20. // 视频背景层(只显示顶部214高度)
  21. Obx(() {
  22. if (controller.isVideoInitialized.value) {
  23. return Positioned(
  24. top: 0,
  25. left: 0,
  26. right: 0,
  27. height: 214.w,
  28. child: ClipRect(
  29. child: FittedBox(
  30. fit: BoxFit.cover,
  31. child: SizedBox(
  32. width: controller.videoController.value.size.width,
  33. height: controller.videoController.value.size.height,
  34. child: VideoPlayer(controller.videoController),
  35. ),
  36. ),
  37. ),
  38. );
  39. }
  40. return const SizedBox.shrink();
  41. }),
  42. // 渐变遮罩层(只在视频区域)
  43. Positioned(
  44. top: 0,
  45. left: 0,
  46. right: 0,
  47. height: 214.w,
  48. child: Container(
  49. decoration: BoxDecoration(
  50. gradient: LinearGradient(
  51. begin: Alignment.topCenter,
  52. end: Alignment.bottomCenter,
  53. colors: [Colors.black.withValues(alpha: 0.6), Colors.black],
  54. stops: const [0.0, 1.0],
  55. ),
  56. ),
  57. ),
  58. ),
  59. // 内容层
  60. SafeArea(
  61. child: Column(
  62. children: [
  63. _buildAppBar(),
  64. Expanded(
  65. child: SingleChildScrollView(
  66. padding: EdgeInsets.symmetric(horizontal: 20.w),
  67. child: Column(
  68. crossAxisAlignment: CrossAxisAlignment.start,
  69. children: [
  70. 16.verticalSpaceFromWidth,
  71. _buildCurrentSubscription(),
  72. 24.verticalSpaceFromWidth,
  73. _buildPlanOptions(),
  74. _buildPlanChangeInfo(),
  75. 16.verticalSpaceFromWidth,
  76. _buildPremiumFeatures(),
  77. 16.verticalSpaceFromWidth,
  78. ],
  79. ),
  80. ),
  81. ),
  82. _buildBottomSection(),
  83. ],
  84. ),
  85. ),
  86. ],
  87. ),
  88. );
  89. }
  90. // 顶部标题栏
  91. Widget _buildAppBar() {
  92. return Padding(
  93. padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h),
  94. child: Row(
  95. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  96. children: [
  97. SizedBox(width: 32.w),
  98. Text(
  99. 'Subscription',
  100. style: TextStyle(
  101. fontSize: 16.sp,
  102. height: 1.4,
  103. fontWeight: FontWeight.w500,
  104. color: Colors.white,
  105. ),
  106. ),
  107. GestureDetector(
  108. onTap: () => Get.back(),
  109. child: Container(
  110. width: 24.w,
  111. height: 24.w,
  112. decoration: BoxDecoration(
  113. color: Colors.white.withValues(alpha: 0.1),
  114. shape: BoxShape.circle,
  115. ),
  116. child: Icon(Icons.close_rounded, color: Colors.white, size: 16.w),
  117. ),
  118. ),
  119. ],
  120. ),
  121. );
  122. }
  123. // 当前订阅信息
  124. Widget _buildCurrentSubscription() {
  125. return Row(
  126. children: [
  127. // 钻石图标
  128. IXImage(
  129. source: Assets.subscriptionDiamond,
  130. width: 92.w,
  131. height: 80.w,
  132. sourceType: ImageSourceType.asset,
  133. ),
  134. 12.horizontalSpace,
  135. Expanded(
  136. child: Column(
  137. crossAxisAlignment: CrossAxisAlignment.start,
  138. children: [
  139. Row(
  140. children: [
  141. IXImage(
  142. source: Assets.subscriptionWallet,
  143. width: 20.w,
  144. height: 20.w,
  145. sourceType: ImageSourceType.asset,
  146. ),
  147. 4.horizontalSpace,
  148. Text(
  149. 'Current subscription',
  150. style: TextStyle(
  151. fontSize: 14.sp,
  152. height: 1.4,
  153. color: DarkThemeColors.subscriptionColor,
  154. fontWeight: FontWeight.w700,
  155. ),
  156. ),
  157. ],
  158. ),
  159. 10.verticalSpaceFromWidth,
  160. Text(
  161. 'Year Plan \$40.00 per year',
  162. style: TextStyle(
  163. fontSize: 14.sp,
  164. height: 1.4,
  165. color: Colors.white,
  166. ),
  167. ),
  168. ],
  169. ),
  170. ),
  171. ],
  172. );
  173. }
  174. // 订阅计划选项
  175. Widget _buildPlanOptions() {
  176. return Obx(
  177. () => Column(
  178. children: List.generate(
  179. controller.plans.length,
  180. (index) => _buildPlanItem(
  181. controller.plans[index],
  182. index,
  183. controller.selectedPlanIndex.value == index,
  184. ),
  185. ),
  186. ),
  187. );
  188. }
  189. Widget _buildPlanItem(Map<String, dynamic> plan, int index, bool isSelected) {
  190. final badge = plan['badge'] as String?;
  191. final badgeBgColor = plan['badgeBgColor'] as Color?;
  192. final badgeTextColor = plan['badgeTextColor'] as Color?;
  193. final badgeBorderColor = plan['badgeBorderColor'] as Color?;
  194. return GestureDetector(
  195. onTap: () => controller.selectPlan(index),
  196. child: Container(
  197. margin: EdgeInsets.only(bottom: 18.w),
  198. decoration: BoxDecoration(
  199. color: DarkThemeColors.cardColor,
  200. borderRadius: BorderRadius.circular(12.r),
  201. border: Border.all(
  202. color: isSelected
  203. ? DarkThemeColors.subscriptionColor
  204. : DarkThemeColors.dividerColor,
  205. width: 2.w,
  206. ),
  207. ),
  208. child: Stack(
  209. clipBehavior: Clip.none,
  210. children: [
  211. // 主要内容
  212. Padding(
  213. padding: EdgeInsets.all(10.w),
  214. child: Row(
  215. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  216. children: [
  217. Column(
  218. crossAxisAlignment: CrossAxisAlignment.start,
  219. children: [
  220. Text(
  221. plan['price'] as String,
  222. style: TextStyle(
  223. fontSize: 18.sp,
  224. height: 1.4,
  225. color: DarkThemeColors.bodyTextColor,
  226. fontWeight: FontWeight.w600,
  227. ),
  228. ),
  229. Text(
  230. plan['period'] as String,
  231. style: TextStyle(
  232. fontSize: 12.sp,
  233. height: 1.6,
  234. color: DarkThemeColors.hintTextColor,
  235. ),
  236. ),
  237. ],
  238. ),
  239. Row(
  240. children: [
  241. Text(
  242. plan['title'] as String,
  243. style: TextStyle(
  244. fontSize: 13.sp,
  245. height: 1.4,
  246. color: DarkThemeColors.bodyTextColor,
  247. ),
  248. ),
  249. 8.horizontalSpace,
  250. Container(
  251. width: 20.w,
  252. height: 20.w,
  253. decoration: BoxDecoration(
  254. shape: BoxShape.circle,
  255. border: Border.all(
  256. color: isSelected
  257. ? DarkThemeColors.primaryColor
  258. : Colors.white30,
  259. width: 1.5.w,
  260. ),
  261. color: isSelected
  262. ? DarkThemeColors.primaryColor
  263. : Colors.transparent,
  264. ),
  265. child: isSelected
  266. ? Icon(Icons.check, color: Colors.white, size: 12.w)
  267. : null,
  268. ),
  269. ],
  270. ),
  271. ],
  272. ),
  273. ),
  274. // 标签固定在右上角,压在边框线上
  275. if (badge != null)
  276. Positioned(
  277. top: -11.h, // 负值让标签向上移动,压在边框线上
  278. right: 12.w,
  279. child: Container(
  280. padding: EdgeInsets.symmetric(horizontal: 6.w),
  281. decoration: BoxDecoration(
  282. color: badgeBgColor ?? Colors.black,
  283. borderRadius: BorderRadius.circular(4.r),
  284. border: badgeBorderColor != null
  285. ? Border.all(color: badgeBorderColor, width: 1)
  286. : null,
  287. ),
  288. child: Text(
  289. badge,
  290. style: TextStyle(
  291. fontSize: 12.sp,
  292. color: badgeTextColor ?? Colors.white,
  293. height: 1.6,
  294. ),
  295. ),
  296. ),
  297. ),
  298. ],
  299. ),
  300. ),
  301. );
  302. }
  303. // 计划变更信息
  304. Widget _buildPlanChangeInfo() {
  305. return InfoCard(
  306. title: 'Plan change info',
  307. items: [
  308. InfoItem(
  309. imageSource: Assets.subscriptionPlanChange1,
  310. title: 'When it starts',
  311. description: 'Your new plan begins right away.',
  312. iconColor: DarkThemeColors.primaryColor,
  313. ),
  314. InfoItem(
  315. imageSource: Assets.subscriptionPlanChange2,
  316. title: 'What happens to your balance',
  317. description:
  318. 'Any unused amount from your old plan will be added to the new one.',
  319. iconColor: DarkThemeColors.primaryColor,
  320. ),
  321. InfoItem(
  322. imageSource: Assets.subscriptionPlanChange3,
  323. title: 'Extra time',
  324. description:
  325. 'You\'ll get extra days based on your remaining balance.',
  326. iconColor: DarkThemeColors.primaryColor,
  327. ),
  328. ],
  329. );
  330. }
  331. // Premium 功能列表
  332. Widget _buildPremiumFeatures() {
  333. return Column(
  334. crossAxisAlignment: CrossAxisAlignment.start,
  335. children: [
  336. Text(
  337. 'Premium\'s included',
  338. style: TextStyle(
  339. fontSize: 16.sp,
  340. color: DarkThemeColors.subscriptionColor,
  341. fontWeight: FontWeight.w500,
  342. ),
  343. ),
  344. 16.verticalSpace,
  345. Container(
  346. padding: EdgeInsets.symmetric(vertical: 4.w, horizontal: 10.w),
  347. decoration: BoxDecoration(
  348. color: DarkThemeColors.bgDisable,
  349. borderRadius: BorderRadius.circular(12.r),
  350. ),
  351. child: Column(
  352. children: [
  353. _buildFeatureItem(IconFont.icon60, 'Unlock all free locations'),
  354. _buildFeatureItem(IconFont.icon61, 'Unlock smart mode'),
  355. _buildFeatureItem(IconFont.icon62, 'Unlock Multi-hop mode'),
  356. _buildFeatureItem(IconFont.icon63, 'Premium can share X devices'),
  357. _buildFeatureItem(IconFont.icon64, 'Own your own private server'),
  358. _buildFeatureItem(IconFont.icon65, 'Close ads'),
  359. ],
  360. ),
  361. ),
  362. ],
  363. );
  364. }
  365. Widget _buildFeatureItem(IconData icon, String title) {
  366. return SizedBox(
  367. height: 44.w,
  368. child: Row(
  369. children: [
  370. Icon(icon, color: DarkThemeColors.subscriptionColor, size: 24.w),
  371. 12.horizontalSpace,
  372. Expanded(
  373. child: Text(
  374. title,
  375. style: TextStyle(
  376. fontSize: 13.sp,
  377. color: Get.reactiveTheme.hintColor,
  378. ),
  379. ),
  380. ),
  381. Container(
  382. width: 20.w,
  383. height: 20.w,
  384. decoration: BoxDecoration(
  385. shape: BoxShape.circle,
  386. color: DarkThemeColors.subscriptionSelectColor,
  387. ),
  388. child: Icon(Icons.check, color: Colors.white, size: 12.w),
  389. ),
  390. ],
  391. ),
  392. );
  393. }
  394. // 底部按钮区域
  395. Widget _buildBottomSection() {
  396. return Container(
  397. padding: EdgeInsets.symmetric(vertical: 10.w, horizontal: 14.w),
  398. decoration: BoxDecoration(
  399. border: Border(
  400. top: BorderSide(color: Colors.white.withOpacity(0.1), width: 1),
  401. ),
  402. ),
  403. child: Column(
  404. mainAxisSize: MainAxisSize.min,
  405. children: [
  406. // 确认按钮
  407. GestureDetector(
  408. onTap: controller.confirmChange,
  409. child: Container(
  410. width: double.infinity,
  411. height: 48.h,
  412. decoration: BoxDecoration(
  413. color: DarkThemeColors.backgroundColor,
  414. borderRadius: BorderRadius.circular(12.r),
  415. ),
  416. child: Center(
  417. child: Text(
  418. 'Confirm Change',
  419. style: TextStyle(
  420. fontSize: 16.sp,
  421. color: DarkThemeColors.subscriptionColor,
  422. fontWeight: FontWeight.w600,
  423. ),
  424. ),
  425. ),
  426. ),
  427. ),
  428. 14.verticalSpaceFromWidth,
  429. // 底部链接
  430. Row(
  431. mainAxisAlignment: MainAxisAlignment.center,
  432. children: [
  433. GestureDetector(
  434. onTap: controller.restorePurchases,
  435. child: Text(
  436. 'Restore Purchases',
  437. style: TextStyle(
  438. fontSize: 16.sp,
  439. color: DarkThemeColors.bodyTextColor,
  440. ),
  441. ),
  442. ),
  443. Text(
  444. ' | ',
  445. style: TextStyle(
  446. fontSize: 16.sp,
  447. color: DarkThemeColors.hintTextColor,
  448. ),
  449. ),
  450. GestureDetector(
  451. onTap: controller.handlePaymentIssue,
  452. child: Text(
  453. 'Payment issue',
  454. style: TextStyle(
  455. fontSize: 16.sp,
  456. color: DarkThemeColors.bodyTextColor,
  457. ),
  458. ),
  459. ),
  460. ],
  461. ),
  462. 14.verticalSpaceFromWidth,
  463. Row(
  464. mainAxisAlignment: MainAxisAlignment.center,
  465. children: [
  466. IXImage(
  467. source: Assets.subscriptionGreenShield,
  468. width: 20.w,
  469. height: 20.w,
  470. sourceType: ImageSourceType.asset,
  471. ),
  472. 10.horizontalSpace,
  473. Text(
  474. 'Yearly auto-renew. Cancel anytime',
  475. style: TextStyle(
  476. fontSize: 13.sp,
  477. color: DarkThemeColors.hintTextColor,
  478. ),
  479. ),
  480. ],
  481. ),
  482. ],
  483. ),
  484. );
  485. }
  486. }