discount_view.dart 17 KB

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