discount_view.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. import 'dart:async';
  2. import 'dart:ffi';
  3. import 'package:intl/intl.dart';
  4. import 'package:clean/data/bean/store_item.dart';
  5. import 'package:clean/base/base_page.dart';
  6. import 'package:clean/module/store/discount/discount_controller.dart';
  7. import 'package:clean/utils/expand.dart';
  8. import 'package:flutter/Material.dart';
  9. import 'package:flutter_screenutil/flutter_screenutil.dart';
  10. import 'package:get/get.dart';
  11. import '../../../resource/assets.gen.dart';
  12. import 'count_down_timer.dart';
  13. import 'func_page_view.dart';
  14. class DiscountPage extends BasePage<DiscountController> {
  15. const DiscountPage({super.key});
  16. @override
  17. bool immersive() {
  18. return true;
  19. }
  20. @override
  21. bool statusBarDarkFont() => false;
  22. @override
  23. Widget buildBody(BuildContext context) {
  24. return Obx(() {
  25. bool isFreeItem =
  26. (controller.currentSelectedStoreItem.value?.freeTrialName != null);
  27. bool isShowFree = isFreeItem && controller.isFree.value;
  28. bool hasUsedFreeTrial = false;
  29. bool canShowFreeTrial = isFreeItem && !hasUsedFreeTrial;
  30. StoreItem? freeItem = controller.storeItems.firstWhereOrNull((element) => element.freeTrialName != null);
  31. return Scaffold(
  32. backgroundColor: "#05050D".color,
  33. body: Stack(
  34. children: [
  35. SafeArea(
  36. child: SingleChildScrollView(
  37. child: Column(
  38. crossAxisAlignment: CrossAxisAlignment.center,
  39. children: [
  40. _AppBar(),
  41. _DiscountHeader(),
  42. SizedBox(height: 26.h),
  43. // 创建一个1分钟的倒计时
  44. CountdownTimer(duration: const Duration(minutes: 1)),
  45. SizedBox(height: 40.h),
  46. if (freeItem != null)
  47. _DiscountFreeTrialSpecialRow(
  48. item: freeItem,
  49. isSelected: canShowFreeTrial,
  50. // canStarFreeTrail: freeItem.freeTrialName != "" && controller.isFree.value,
  51. canStarFreeTrail: true,
  52. onSelect: (title) {
  53. controller.currentSelectedStoreItem.value = freeItem;
  54. },
  55. ),
  56. SizedBox(height: 40.h),
  57. _FeaturesPreview(),
  58. SizedBox(height: 40.h),
  59. _MorePlansSection(),
  60. SizedBox(height: 5.h),
  61. _OtherPlansSection(items: controller.storeItems.where((element) => element.freeTrialName == null).toList(), controller: controller),
  62. SizedBox(height: 100.h),
  63. ],
  64. ),
  65. ),
  66. ),
  67. SafeArea(
  68. top: false,
  69. child: Column(
  70. mainAxisSize: MainAxisSize.max,
  71. children: [
  72. Spacer(),
  73. Container(
  74. width: double.infinity,
  75. child: _PurchaseSection(
  76. isShowFree: canShowFreeTrial,
  77. controller: controller,
  78. ),
  79. )
  80. ],
  81. ),
  82. ),
  83. ],
  84. ),
  85. );
  86. });
  87. }
  88. }
  89. class _AppBar extends StatelessWidget {
  90. const _AppBar({Key? key}) : super(key: key);
  91. @override
  92. Widget build(BuildContext context) {
  93. return Row(
  94. children: [
  95. Container(
  96. margin: EdgeInsets.only(left: 16.w, top: 14.h),
  97. child: GestureDetector(
  98. onTap: () {
  99. Get.back();
  100. },
  101. child: Assets.images.iconStoreClose
  102. .image(width: 28.w, height: 28.w),
  103. ),
  104. ),
  105. ],
  106. );
  107. }
  108. }
  109. class _DiscountHeader extends StatelessWidget {
  110. const _DiscountHeader({Key? key}) : super(key: key);
  111. @override
  112. Widget build(BuildContext context) {
  113. return Column(
  114. children: [
  115. Assets.images.iconDiscountTitle
  116. .image(width: 259.w, height: 55.h),
  117. SizedBox(height: 20.h),
  118. Assets.images.iconDiscountPercent
  119. .image(width: 195.w, height: 86.h),
  120. SizedBox(height: 13.h),
  121. Container(
  122. width: 197.w,
  123. height: 32.h,
  124. padding: EdgeInsets.all(1.w),
  125. decoration: BoxDecoration(
  126. gradient: LinearGradient(
  127. begin: Alignment.topCenter,
  128. end: Alignment.bottomCenter,
  129. colors: [
  130. '#CF9EFD'.color,
  131. '#4DCFFF'.color.withOpacity(0.5),
  132. ],
  133. ),
  134. borderRadius: BorderRadius.all(Radius.circular(18.r)),
  135. ),
  136. child: Container(
  137. decoration: BoxDecoration(
  138. color: "#05050D".color,
  139. borderRadius: BorderRadius.all(Radius.circular(18.r)),
  140. ),
  141. child: Center(
  142. child: Text(
  143. "Get CleanPro Premium",
  144. style: TextStyle(
  145. color: Colors.white,
  146. fontSize: 15.sp,
  147. fontWeight: FontWeight.w700,
  148. ),
  149. ),
  150. ),
  151. ),
  152. ),
  153. ],
  154. );
  155. }
  156. }
  157. class _FeaturesPreview extends StatelessWidget {
  158. const _FeaturesPreview({Key? key}) : super(key: key);
  159. @override
  160. Widget build(BuildContext context) {
  161. return InfinitePreviewPageView(
  162. height: 98.h,
  163. autoPlay: true,
  164. autoPlayDuration: const Duration(seconds: 3),
  165. items: [
  166. PreviewItem(
  167. title: 'One-click Remove Similar Photos',
  168. icon: Assets.images.iconStoreSimilar.image(),
  169. ),
  170. PreviewItem(
  171. title: 'Detect Blurry and Junk Photos',
  172. icon: Assets.images.iconStoreAi.image(),
  173. ),
  174. PreviewItem(
  175. title: 'Merge Duplicate Contacts',
  176. icon: Assets.images.iconStoreContacts.image(),
  177. ),
  178. PreviewItem(
  179. title: 'Premium Unlimited',
  180. icon: Assets.images.iconStorePremium.image(),
  181. ),
  182. ],
  183. showIndicator: true,
  184. );
  185. }
  186. }
  187. class _PurchaseSection extends StatelessWidget {
  188. final bool isShowFree;
  189. final DiscountController controller;
  190. const _PurchaseSection({
  191. Key? key,
  192. required this.isShowFree,
  193. required this.controller,
  194. }) : super(key: key);
  195. @override
  196. Widget build(BuildContext context) {
  197. return Container(
  198. decoration: BoxDecoration(
  199. gradient: LinearGradient(
  200. begin: Alignment.topCenter,
  201. end: Alignment.bottomCenter,
  202. colors: [
  203. "000000".color.withOpacity(0),
  204. "000000".color,
  205. ],
  206. ),
  207. ),
  208. child: Column(
  209. children: [
  210. Text(
  211. isShowFree
  212. ? ""
  213. : "Auto-renewalable.Cancel anytime",
  214. style: TextStyle(
  215. color: Colors.white.withOpacity(0.8),
  216. fontSize: 12.sp,
  217. ),
  218. ),
  219. SizedBox(height: 1.h),
  220. GestureDetector(
  221. onTap: () {
  222. controller.onBuyClick();
  223. },
  224. child: Container(
  225. width: 312.w,
  226. height: 48.h,
  227. decoration: BoxDecoration(
  228. color: "#0279FB".color,
  229. borderRadius: BorderRadius.all(
  230. Radius.circular(24.r),
  231. ),
  232. ),
  233. child: Center(
  234. child: Text(
  235. isShowFree ? "START FREE TRIAL" : "Continue",
  236. style: TextStyle(
  237. color: Colors.white,
  238. fontWeight: FontWeight.w700,
  239. fontSize: 16.sp,
  240. ),
  241. ),
  242. ),
  243. ),
  244. ),
  245. SizedBox(height: 5.h),
  246. isShowFree ?
  247. Text("No payment now!",
  248. style: TextStyle(
  249. color: "#57C87A".color,
  250. fontSize: 12.sp,
  251. fontWeight: FontWeight.w500,
  252. ),
  253. )
  254. :
  255. Container(
  256. alignment: Alignment.center,
  257. width: double.infinity,
  258. child: Row(
  259. mainAxisAlignment: MainAxisAlignment.center,
  260. children: [
  261. GestureDetector(
  262. onTap: () {
  263. controller.onBuyClick();
  264. },
  265. child: Text("Privacy Policy", style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 12.sp, fontWeight: FontWeight.w400)),
  266. ),
  267. SizedBox(width: 8.w),
  268. Text("|", style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 12.sp, fontWeight: FontWeight.w400)),
  269. SizedBox(width: 8.w),
  270. GestureDetector(
  271. onTap: () {
  272. controller.onBuyClick();
  273. },
  274. child: Text("Privacy Policy", style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 12.sp, fontWeight: FontWeight.w400)),
  275. ),
  276. ],
  277. ),
  278. )
  279. ],
  280. ),
  281. );
  282. }
  283. }
  284. class _MorePlansSection extends StatelessWidget {
  285. const _MorePlansSection({Key? key}) : super(key: key);
  286. @override
  287. Widget build(BuildContext context) {
  288. return Container(
  289. width: double.infinity,
  290. child: Row(
  291. mainAxisAlignment: MainAxisAlignment.center,
  292. children: [
  293. Assets.images.iconStoreMoreArrow.image(width: 8.w, height: 8.w),
  294. SizedBox(width: 5.w),
  295. Text("More Plans", style: TextStyle(color: 'ffffff'.color.withOpacity(0.7), fontSize: 12.sp, fontWeight: FontWeight.w400)),
  296. SizedBox(width: 5.w),
  297. Assets.images.iconStoreMoreArrow.image(width: 8.w, height: 8.w),
  298. ],
  299. ),
  300. );
  301. }
  302. }
  303. class _OtherPlansSection extends StatelessWidget {
  304. final List<StoreItem> items;
  305. final DiscountController controller;
  306. const _OtherPlansSection({Key? key, required this.items, required this.controller}) : super(key: key);
  307. @override
  308. Widget build(BuildContext context) {
  309. return Column(
  310. crossAxisAlignment: CrossAxisAlignment.center,
  311. children: [
  312. ...items.map((item) => Padding(
  313. padding: EdgeInsets.only(bottom: 12.h),
  314. child: _DiscountFreeTrialSpecialRow(
  315. item: item,
  316. isSelected: controller.currentSelectedStoreItem.value?.id == item.id,
  317. canStarFreeTrail: item.freeTrialName != null && controller.isFree.value,
  318. onSelect: (item) {
  319. controller.currentSelectedStoreItem.value = item;
  320. },
  321. ),
  322. )).toList(),
  323. ],
  324. );
  325. }
  326. }
  327. class _DiscountFreeTrialSpecialRow extends StatelessWidget {
  328. final StoreItem item;
  329. final bool isSelected;
  330. final bool canStarFreeTrail;
  331. final Function(StoreItem) onSelect;
  332. const _DiscountFreeTrialSpecialRow({Key? key, required this.item, required this.isSelected, required this.canStarFreeTrail, required this.onSelect}) : super(key: key);
  333. @override
  334. Widget build(BuildContext context) {
  335. // 价格信息容器
  336. Widget buildPriceInfoContainer() {
  337. return Positioned(
  338. bottom: -18.h,
  339. child: Container(
  340. alignment: Alignment.bottomCenter,
  341. height: 43.h,
  342. decoration: BoxDecoration(
  343. color: '#1B2231'.color,
  344. borderRadius: BorderRadius.circular(12.r),
  345. ),
  346. child: Padding(
  347. padding: EdgeInsets.only(top: 14.h, left: 7.w, right: 9.w, bottom: 2.h),
  348. child: Text(
  349. item.freeTrialPriceDesc ?? item.priceDesc ?? "",
  350. style: TextStyle(
  351. color: '#ffffff'.color.withOpacity(0.7),
  352. fontSize: 12,
  353. fontWeight: FontWeight.w400
  354. )
  355. ),
  356. )
  357. ),
  358. );
  359. }
  360. // 试用名称内容
  361. Widget buildTrialNameContent() {
  362. return Padding(
  363. padding: EdgeInsets.symmetric(vertical: 7.h, horizontal: 12.w),
  364. child: SizedBox(
  365. width: double.infinity,
  366. child: Column(
  367. mainAxisAlignment: MainAxisAlignment.center,
  368. crossAxisAlignment: CrossAxisAlignment.start,
  369. children: [
  370. Text(
  371. item.freeTrialName ?? item.name,
  372. style: TextStyle(
  373. color: isSelected ? '#FFe168'.color : '#ffffff'.color,
  374. fontSize: 22.sp,
  375. fontWeight: FontWeight.w900,
  376. ),
  377. ),
  378. ],
  379. ),
  380. ),
  381. );
  382. }
  383. Widget buildNormalProductContent() {
  384. final formatter = NumberFormat.currency(
  385. symbol: '\$',
  386. decimalDigits: 2,
  387. );
  388. var amount = formatter.format(item.amount / 100);
  389. return Padding(
  390. padding: EdgeInsets.symmetric(vertical: 7.h, horizontal: 12.w),
  391. child: Row(
  392. children: [
  393. Column(
  394. crossAxisAlignment: CrossAxisAlignment.start,
  395. mainAxisAlignment: MainAxisAlignment.center,
  396. children: [
  397. Text(item.name, style: TextStyle(color: '#ffffff'.color, fontSize: 16.sp, fontWeight: FontWeight.w700)),
  398. Text(item.priceDesc ?? "", style: TextStyle(color: '#ffffff'.color.withOpacity(0.6), fontSize: 12.sp, fontWeight: FontWeight.w400)),
  399. ],
  400. ),
  401. Spacer(),
  402. Text(amount, style: TextStyle(color: '#ffffff'.color, fontSize: 16.sp, fontWeight: FontWeight.w700)),
  403. ],
  404. )
  405. );
  406. }
  407. // 免费标签的容器
  408. Widget buildBadgeContainer() {
  409. return Container(
  410. padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
  411. decoration: BoxDecoration(
  412. gradient: LinearGradient(
  413. begin: Alignment.centerLeft,
  414. end: Alignment.centerRight,
  415. colors: [
  416. '#53CDFE'.color,
  417. '#0279FB'.color,
  418. ],
  419. ),
  420. borderRadius: BorderRadius.all(Radius.circular(20.w))
  421. ),
  422. child: Text(
  423. 'No payment now!',
  424. style: TextStyle(
  425. color: Colors.white,
  426. fontSize: 12.sp,
  427. fontWeight: FontWeight.w700,
  428. ),
  429. ),
  430. );
  431. }
  432. // 免费标签的图标
  433. Widget buildBadgeIcon() {
  434. return Positioned(
  435. left: -22,
  436. child: Assets.images.iconStoreFree.image(width: 30.w, height: 30.w)
  437. );
  438. }
  439. // 免费试用标签
  440. Widget buildFreeTrialBadge() {
  441. return Positioned(
  442. top: -14,
  443. right: 10,
  444. child: Stack(
  445. clipBehavior: Clip.none,
  446. alignment: Alignment.centerLeft,
  447. children: [
  448. buildBadgeContainer(),
  449. buildBadgeIcon(),
  450. ],
  451. )
  452. );
  453. }
  454. // 主容器
  455. Widget buildMainContainer() {
  456. return Container(
  457. padding: EdgeInsets.all(2),
  458. decoration: isSelected ? BoxDecoration(
  459. gradient: LinearGradient(
  460. begin: Alignment.topLeft,
  461. end: Alignment.bottomRight,
  462. colors: [
  463. '#D2A3FF'.color,
  464. '#419CFF'.color,
  465. '#01D0FF'.color,
  466. ]
  467. ),
  468. borderRadius: BorderRadius.circular(20.r),
  469. ) : BoxDecoration(
  470. color: '#ffffff'.color.withOpacity(0.2),
  471. borderRadius: BorderRadius.circular(20.r),
  472. ),
  473. child: Container(
  474. height: 74.w,
  475. decoration: BoxDecoration(
  476. color: isSelected ? '#111f4b'.color : '000000'.color,
  477. borderRadius: BorderRadius.circular(18.r),
  478. ),
  479. child: Stack(
  480. clipBehavior: Clip.none,
  481. children: [
  482. if (canStarFreeTrail) buildTrialNameContent() else buildNormalProductContent(),
  483. // 右上角FREE标签
  484. if (canStarFreeTrail) buildFreeTrialBadge(),
  485. ],
  486. ),
  487. ),
  488. );
  489. }
  490. return Container(
  491. margin: EdgeInsets.symmetric(horizontal: 15.w),
  492. child: GestureDetector(
  493. onTap: () {
  494. onSelect(item);
  495. },
  496. child: Stack(
  497. alignment: Alignment.bottomRight,
  498. clipBehavior: Clip.none,
  499. children: [
  500. if (canStarFreeTrail) buildPriceInfoContainer(),
  501. buildMainContainer(),
  502. ],
  503. ),
  504. ),
  505. );
  506. }
  507. }