challenge_checkin.dart 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. import 'dart:async';
  2. import 'package:fast/model/model.dart';
  3. import 'package:fast/utils/api.dart';
  4. import 'package:fast/utils/global.dart';
  5. import 'package:fast/utils/http_utils.dart';
  6. import 'package:fast/utils/size_fit.dart';
  7. import 'package:fast/utils/util.dart';
  8. import 'package:fast/view/component/fast_btn.dart';
  9. import 'package:fast/view/component/toast.dart';
  10. import 'package:flutter/material.dart';
  11. import 'challenge_checkout.dart';
  12. import 'challenge_result.dart';
  13. // ignore: must_be_immutable
  14. GlobalKey<CheckinItemState> checkinKey2 = GlobalKey();
  15. // ignore: must_be_immutable
  16. class FastCheckin extends StatefulWidget {
  17. bool abandon;
  18. bool supertimeout;
  19. FastBean fast;
  20. int timestamp;
  21. Map<String, dynamic> checkInfo;
  22. FastCheckin(
  23. {Key? key,
  24. required this.abandon,
  25. required this.supertimeout,
  26. required this.fast,
  27. required this.timestamp,
  28. required this.checkInfo})
  29. : super(key: key);
  30. @override
  31. State<FastCheckin> createState() => _FastCheckinState();
  32. }
  33. class _FastCheckinState extends State<FastCheckin>
  34. with TickerProviderStateMixin {
  35. late AnimationController controller2, controller3;
  36. late Animation aniStep2;
  37. late AnimationController animationController;
  38. late Animation step0, step1, step2, step3, step4, step5;
  39. bool rightTime = false;
  40. FastResultBean? resultBean;
  41. String desc1 = '', desc2 = '', desc3 = '';
  42. // Key<CheckinItemState> key =
  43. // ObjectKey checkinKey = const ObjectKey('item');
  44. // LocalKey<CheckinItemState> key = LocalKey();
  45. List checkList = [];
  46. bool btnEnable = true;
  47. int earnings = 0;
  48. int index = 1;
  49. int step = 0; //动画执行步骤
  50. late DateTime date;
  51. @override
  52. void initState() {
  53. super.initState();
  54. date = serverTime();
  55. controller2 = AnimationController(
  56. vsync: this, duration: const Duration(milliseconds: 300));
  57. controller3 = AnimationController(
  58. vsync: this, duration: const Duration(milliseconds: 1200));
  59. animationController = AnimationController(
  60. vsync: this, duration: const Duration(milliseconds: 3000));
  61. step0 = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(
  62. parent: animationController,
  63. curve: const Interval(0.0, 300.0 / 3000.0)));
  64. step1 = Tween(begin: 1.0, end: 1.1).animate(CurvedAnimation(
  65. parent: animationController,
  66. curve: const Interval(300.0 / 3000.0, 1500.0 / 3000.0)));
  67. step2 = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(
  68. parent: animationController,
  69. curve: const Interval(1500 / 3000, 1800.0 / 3000.0)));
  70. step3 = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(
  71. parent: animationController,
  72. curve: const Interval(1800 / 3000, 2100.0 / 3000.0)));
  73. step4 = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(
  74. parent: animationController,
  75. curve: const Interval(2100.0 / 3000, 2400.0 / 3000.0)));
  76. step5 = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(
  77. parent: animationController,
  78. curve: const Interval(2400.0 / 3000, 2700.0 / 3000.0)));
  79. step0.addListener(() {
  80. if (step0.value == 1.0) {
  81. setState(() {
  82. step = 1;
  83. });
  84. } else {}
  85. });
  86. step1.addListener(() {
  87. if (step1.value == 1.1) {
  88. setState(() {
  89. step = 2;
  90. });
  91. }
  92. });
  93. animationController.addListener(() {
  94. setState(() {});
  95. });
  96. animationController.forward();
  97. /*
  98. 动画步骤
  99. step 0:
  100. 状态图片从上到下 alpha 0-1
  101. step 1:
  102. 进食时长从下到上 alpha 0-1
  103. step 2:
  104. 页面停留 1s
  105. step 3:
  106. 状态图片从下到上推一点 alpha 保持不变
  107. 进食时长从上到下 alpha 1-0
  108. step 4:
  109. 打卡从上到下 alpha 0-1
  110. step 5:
  111. 描述 从上到下 alpha 0-1
  112. step 4-5:
  113. 底部按钮 alpha 0-1
  114. */
  115. aniStep2 = Tween(begin: 1.0, end: 0.0).animate(controller2);
  116. controller2.addListener(() {
  117. setState(() {});
  118. });
  119. controller3.addListener(() {
  120. setState(() {});
  121. });
  122. Timer(const Duration(milliseconds: 2000), () {
  123. setState(() {
  124. step = 1;
  125. controller2.forward();
  126. });
  127. });
  128. getCheckinData();
  129. if (widget.abandon) {
  130. giveUp();
  131. }
  132. }
  133. DateTime serverTime() {
  134. int milliseconds = DateTime.now().millisecondsSinceEpoch;
  135. milliseconds = milliseconds + Global().timeSeconds * 1000;
  136. return DateTime.fromMillisecondsSinceEpoch(milliseconds);
  137. }
  138. Future giveUp() async {
  139. Map<String, dynamic> data = await HttpUtils.post(Api.end,
  140. data: {'real_end_time': serverTime().millisecondsSinceEpoch ~/ 1000});
  141. resultBean = FastResultBean.fromJson(data);
  142. }
  143. void getCheckinData() {
  144. Map<String, dynamic> data = widget.checkInfo;
  145. setState(() {
  146. rightTime = data['checkin_status'] == 'SUCCESS';
  147. if (data['checkin_status'] == 'SUCCESS') {
  148. desc1 = '第${data['current_day_index']}天开始断食';
  149. desc2 = Util().checkFormateDate(
  150. DateTime.fromMillisecondsSinceEpoch(
  151. (data['start_time'] as int) * 1000),
  152. serverTime()) +
  153. '~' +
  154. Util().checkFormateDate(
  155. DateTime.fromMillisecondsSinceEpoch(
  156. (data['end_time'] as int) * 1000),
  157. serverTime()) +
  158. ',计划断食' +
  159. Util().betweenTime(
  160. DateTime.fromMillisecondsSinceEpoch(
  161. (data['start_time'] as int) * 1000),
  162. DateTime.fromMillisecondsSinceEpoch(
  163. (data['end_time'] as int) * 1000));
  164. desc3 = Util().checkFormateDate(
  165. DateTime.fromMillisecondsSinceEpoch(
  166. (data['checkin_expire'] as int) * 1000),
  167. serverTime()) +
  168. '前完成打卡,视为准时打卡';
  169. } else {
  170. desc1 = '第${data['current_day_index']}天开始断食';
  171. desc2 = Util().checkFormateDate(
  172. DateTime.fromMillisecondsSinceEpoch(
  173. (data['start_time'] as int) * 1000),
  174. serverTime()) +
  175. '~' +
  176. Util().checkFormateDate(
  177. DateTime.fromMillisecondsSinceEpoch(
  178. (data['end_time'] as int) * 1000),
  179. serverTime()) +
  180. ',计划断食' +
  181. Util().betweenTime(
  182. DateTime.fromMillisecondsSinceEpoch(
  183. (data['start_time'] as int) * 1000),
  184. DateTime.fromMillisecondsSinceEpoch(
  185. (data['end_time'] as int) * 1000));
  186. desc3 = Util().checkFormateDate(
  187. DateTime.fromMillisecondsSinceEpoch(
  188. (data['checkin_invalid'] as int) * 1000),
  189. serverTime()) +
  190. '前完成打卡,视为超时打卡';
  191. }
  192. if (widget.supertimeout) {
  193. desc1 = '第${data['current_day_index']}天结束断食·打卡失效\n挑战终止,欢迎再战';
  194. desc3 = Util().checkFormateDate(
  195. DateTime.fromMillisecondsSinceEpoch(
  196. (data['checkout_invalid'] as int) * 1000),
  197. serverTime()) +
  198. '前未完成打卡,打卡已失效';
  199. }
  200. });
  201. List checkinDays = data['checkin_days'];
  202. List<ChallengeCheckinBean> list = [];
  203. for (int i = 0; i < data['days']; i++) {
  204. ChallengeCheckinBean bean = ChallengeCheckinBean();
  205. bean.day = i + 1;
  206. if (i < checkinDays.length &&
  207. checkinDays[i]['checkin_status'] != 'PENDING') {
  208. bean.stone = checkinDays[i]['rjv_earnings'];
  209. bean.passed = true;
  210. }
  211. if (i == data['current_day_index'] - 1) {
  212. bean.today = true;
  213. if (widget.abandon || widget.supertimeout) {
  214. bean.abandon = true;
  215. }
  216. }
  217. list.add(bean);
  218. }
  219. List array = data['days'] == 7
  220. ? [
  221. [list[0], list[1], list[2], list[3]],
  222. [list[4], list[5], list[6]]
  223. ]
  224. : [list];
  225. setState(() {
  226. checkList = array;
  227. index = data['current_day_index'];
  228. earnings = data['rjv_earnings'];
  229. });
  230. }
  231. @override
  232. void dispose() {
  233. Global().showCheckin = false;
  234. controller2.dispose();
  235. controller3.dispose();
  236. animationController.dispose();
  237. super.dispose();
  238. }
  239. @override
  240. Widget build(BuildContext context) {
  241. SizeFit.initialize(context);
  242. double screenHeight = MediaQuery.of(context).size.height;
  243. List<Widget> widgets = [];
  244. for (int i = 0; i < earnings; i++) {
  245. widgets.add(Container(
  246. margin: EdgeInsets.only(left: 2.px),
  247. child: Image.asset(
  248. 'assets/images/stone.png',
  249. width: 12.px,
  250. height: 12.px,
  251. ),
  252. ));
  253. }
  254. double value1 = step == 0 ? step0.value : (1.0 - step2.value);
  255. double valueTop = step == 0
  256. ? 0.14 * screenHeight + 0.1 * screenHeight * step0.value
  257. : 0.14 * screenHeight + 0.1 * screenHeight * (1 - step2.value);
  258. int eatTime = 24 * 3600 - (widget.fast.endTime! - widget.fast.startTime!);
  259. int eatHour = eatTime ~/ 3600;
  260. int eatMinute = eatTime % 3600 ~/ 60;
  261. String str = eatMinute > 0 ? eatMinute.toString() + '分钟' : '';
  262. String strEatTime = eatHour.toString() + '小时' + str;
  263. return Container(
  264. decoration: BoxDecoration(
  265. image: DecorationImage(
  266. image: AssetImage('assets/images/${widget.fast.days}days_bg.png'),
  267. fit: BoxFit.cover),
  268. ),
  269. alignment: Alignment.center,
  270. child: Column(
  271. children: [
  272. Opacity(
  273. opacity: step0.value,
  274. child: Container(
  275. margin: EdgeInsets.only(top: valueTop),
  276. child: Column(
  277. children: [
  278. Opacity(
  279. opacity: 0.4,
  280. child: Image.asset(
  281. 'assets/images/challenge_mode.png',
  282. width: 298.px,
  283. height: 28.px,
  284. ),
  285. ),
  286. SizedBox(
  287. height: 12.px,
  288. ),
  289. Image.asset(
  290. 'assets/images/challenge_${widget.fast.days}.png',
  291. width: 207.px,
  292. height: 96.px,
  293. )
  294. ],
  295. ),
  296. ),
  297. ),
  298. Stack(
  299. children: [
  300. // Opacity(
  301. // opacity: value1,
  302. // child: Container(
  303. // alignment: Alignment.center,
  304. // child: Column(
  305. // mainAxisAlignment: MainAxisAlignment.center,
  306. // children: [
  307. // SizedBox(
  308. // height: 8.px + (1 - value1) * 200.px,
  309. // ),
  310. // Text(
  311. // '第${widget.fast.currentDayIndex}天进食结束',
  312. // textAlign: TextAlign.center,
  313. // style: TextStyle(
  314. // color: const Color(0xFFFF9B00),
  315. // fontSize: 36.px,
  316. // fontWeight: FontWeight.bold),
  317. // ),
  318. // SizedBox(height: 12.px),
  319. // Text('进食时长$strEatTime',
  320. // textAlign: TextAlign.center,
  321. // style:
  322. // TextStyle(color: Colors.white, fontSize: 16.px)),
  323. // ],
  324. // ),
  325. // ),
  326. // ),
  327. Opacity(
  328. opacity: step4.value,
  329. child: Container(
  330. alignment: Alignment.center,
  331. child: Column(
  332. mainAxisAlignment: MainAxisAlignment.center,
  333. children: [
  334. SizedBox(
  335. height: checkList.length == 1
  336. ? 95.px
  337. : 120.px + 20.px * step4.value,
  338. ),
  339. Text(
  340. desc1,
  341. // '第 ${widget.fast.currentDayIndex} 天开始断食打卡签到准时',
  342. textAlign: TextAlign.center,
  343. style: TextStyle(
  344. color: (widget.abandon || widget.supertimeout)
  345. ? const Color(0xFFFF0000)
  346. : const Color(0xFFC4CCDA),
  347. fontSize: (widget.abandon || widget.supertimeout)
  348. ? 12.px
  349. : 13.px),
  350. ),
  351. SizedBox(
  352. height: 5.px,
  353. ),
  354. Text(
  355. (widget.abandon || widget.supertimeout) ? '' : desc2,
  356. // '每晚餐后开始断食打卡,次日早餐前\n结束断食打卡,你已连续坚持 2 天',
  357. textAlign: TextAlign.center,
  358. style: TextStyle(
  359. color: const Color(0x99C4CCDA), fontSize: 12.px),
  360. )
  361. ],
  362. ),
  363. ),
  364. ),
  365. Opacity(
  366. opacity: step3.value,
  367. child: Container(
  368. alignment: Alignment.center,
  369. margin: EdgeInsets.only(top: 12.px * step3.value),
  370. child: Column(
  371. mainAxisAlignment: MainAxisAlignment.center,
  372. crossAxisAlignment: CrossAxisAlignment.center,
  373. children: List<Widget>.generate(
  374. checkList.length,
  375. (index) {
  376. return Container(
  377. height: 50.px,
  378. width: checkList[index].length * 50.px,
  379. margin: EdgeInsets.only(bottom: 8.px),
  380. decoration: BoxDecoration(
  381. color:
  382. const Color.fromARGB(38, 196, 204, 218),
  383. borderRadius:
  384. BorderRadius.all(Radius.circular(25.px))),
  385. child: Row(
  386. children: List<Widget>.generate(
  387. checkList[index].length, (j) {
  388. return CheckinItem(
  389. key: checkList[index][j].today
  390. ? checkinKey2
  391. : null,
  392. bean: checkList[index][j],
  393. isCheckin: true,
  394. );
  395. }),
  396. ),
  397. );
  398. },
  399. ),
  400. ))),
  401. ],
  402. ),
  403. Expanded(
  404. child: SizedBox(
  405. height: 0.px,
  406. )),
  407. Opacity(
  408. opacity: step4.value,
  409. child: Column(
  410. children: [
  411. if (widget.supertimeout || widget.abandon)
  412. FastBtn(
  413. title: '确定',
  414. disable: btnEnable ? false : true,
  415. width: 228.px,
  416. height: 48.px,
  417. callback: () {
  418. if (Global().allowNotification == false &&
  419. Global().pushEnable) {
  420. Util().showNotificationStatus(context);
  421. return;
  422. }
  423. if (widget.abandon) {
  424. endConfirm();
  425. } else if (widget.supertimeout) {
  426. superTimeoutEnd();
  427. } else {
  428. checkin();
  429. // checkinKey.currentState!.checkin();
  430. setState(() {
  431. btnEnable = false;
  432. });
  433. }
  434. }),
  435. if (!widget.supertimeout && !widget.abandon)
  436. FastBtn(
  437. title: "",
  438. img: Image.asset(
  439. 'assets/images/checkin.png',
  440. width: 132.px,
  441. height: 26.px,
  442. ),
  443. disable: !btnEnable,
  444. width: 228.px,
  445. height: 48.px,
  446. callback: () {
  447. if (Global().allowNotification == false &&
  448. Global().pushEnable) {
  449. Util().showNotificationStatus(context);
  450. return;
  451. }
  452. if (widget.abandon) {
  453. endConfirm();
  454. } else if (widget.supertimeout) {
  455. superTimeoutEnd();
  456. } else {
  457. checkin();
  458. // checkinKey.currentState!.checkin();
  459. setState(() {
  460. btnEnable = false;
  461. });
  462. }
  463. }),
  464. SizedBox(
  465. height: 17.px,
  466. ),
  467. Text(
  468. desc3, //'「结束断食」打卡后,即可获得逆龄石',
  469. style: TextStyle(
  470. color: const Color(0x99C4CCDA), fontSize: 12.px),
  471. ),
  472. SizedBox(
  473. height: 0.07 * screenHeight * step4.value,
  474. )
  475. ],
  476. ),
  477. )
  478. ],
  479. ),
  480. );
  481. }
  482. Future superTimeoutEnd() async {
  483. Map<String, dynamic> data = await HttpUtils.post(Api.checkin,
  484. data: {'day_index': index, 'checkin_time': widget.timestamp});
  485. resultBean = FastResultBean.fromJson(data);
  486. showResultPage();
  487. }
  488. Future endConfirm() async {
  489. await HttpUtils.post(Api.endConfirm, data: {});
  490. showResultPage();
  491. }
  492. showResultPage() {
  493. Navigator.of(context).pop();
  494. Global().homePage.closeCheckPop();
  495. showDialog(
  496. context: context,
  497. barrierDismissible: false,
  498. barrierColor: Colors.transparent,
  499. useSafeArea: false,
  500. builder: (BuildContext context) {
  501. return ChallengeResult(resultBean: resultBean!);
  502. });
  503. }
  504. Future checkin() async {
  505. Map<String, dynamic> data = await HttpUtils.post(Api.checkin,
  506. data: {'day_index': index, 'checkin_time': widget.timestamp});
  507. resultBean = FastResultBean.fromJson(data);
  508. checkinKey2.currentState!.checkin(false);
  509. if (resultBean!.over!) {
  510. Timer(const Duration(milliseconds: 2500), () {
  511. showResultPage();
  512. });
  513. } else {
  514. Timer(const Duration(milliseconds: 2500), () {
  515. Navigator.of(context).pop();
  516. showDialog(
  517. context: context,
  518. barrierDismissible: false,
  519. barrierColor: Colors.transparent,
  520. builder: (BuildContext context) {
  521. return Toast(
  522. title: rightTime ? '准时打卡' : '超时打卡',
  523. content: const SizedBox(
  524. width: 0,
  525. ),
  526. );
  527. });
  528. Global().homePage.closeCheckPop();
  529. if (widget.supertimeout) {
  530. showResultPage();
  531. return;
  532. } else {
  533. //正常打卡
  534. }
  535. });
  536. Global().homePage.checkUpdate();
  537. }
  538. }
  539. }