discount_view.dart 16 KB

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