home_screen.dart 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:fluttertoast/fluttertoast.dart';
  4. import 'package:image_puzzle/audio/audio_controller.dart';
  5. import 'package:image_puzzle/collection/collection_screen.dart';
  6. import 'package:image_puzzle/config/device.dart';
  7. import 'package:image_puzzle/homepage/home_board.dart';
  8. import 'package:image_puzzle/homepage/home_board_play.dart';
  9. import 'package:image_puzzle/models/cached_request.dart';
  10. import 'package:image_puzzle/models/data.dart';
  11. import 'package:image_puzzle/models/items.dart';
  12. import 'package:image_puzzle/play/board_play.dart';
  13. import 'package:image_puzzle/settings/settings_dialog.dart';
  14. import 'package:image_puzzle/skin/skin.dart';
  15. import 'package:image_puzzle/utils/mybutton.dart';
  16. import 'package:logging/logging.dart';
  17. import 'package:lottie/lottie.dart';
  18. import 'package:provider/provider.dart';
  19. final Logger _log = Logger('home_screen');
  20. class HomeScreen extends StatefulWidget {
  21. const HomeScreen({super.key});
  22. @override
  23. State<StatefulWidget> createState() => _HomeScreen();
  24. }
  25. class _HomeScreen extends State<HomeScreen> {
  26. late AudioController audio;
  27. late Data data;
  28. List<ListItem>? latest;
  29. late CachedRequest latestCachedRequest;
  30. late StreamSubscription? latestSubscription;
  31. // 自定义画布控制器(可选,用于控制画布绘制逻辑)
  32. final _canvasKey = GlobalKey<HomeBoardPlayState>();
  33. // !!! 新增:用于定位 Collection 按钮的 GlobalKey
  34. final GlobalKey _collectionKey = GlobalKey();
  35. bool get isLoading => currentItem == null;
  36. @override
  37. void initState() {
  38. super.initState();
  39. audio = context.read<AudioController>();
  40. data = context.read<Data>();
  41. latestCachedRequest = data.latest;
  42. // 主动获取缓存数据(关键)
  43. final cachedData = latestCachedRequest.cachedData;
  44. if (cachedData != null) {
  45. _onLatestDataUpdate(cachedData);
  46. }
  47. latestSubscription = latestCachedRequest.stream.listen(_onLatestDataUpdate, onError: _onLatestDataError);
  48. }
  49. @override
  50. void dispose() {
  51. latestSubscription?.cancel();
  52. super.dispose();
  53. }
  54. _onLatestDataUpdate(data) {
  55. _log.info('_onLatestDataUpdate.... ');
  56. if (data != null) {
  57. latest = data as List<ListItem>;
  58. setState(() {});
  59. if (data.length >= 20) {
  60. // 远程latest列表已加载,说明网络已通,这个时候再来初始化Admod,ATT, UMP这些东西
  61. initThird();
  62. }
  63. }
  64. }
  65. _onLatestDataError(error) {
  66. _log.info('_onLatestDataError.... $error');
  67. if (latest == null || latest!.isEmpty || latest!.length < 20) {
  68. // 列表数据如果少于20,说明只是内置图,仍然刷新远程请求
  69. _log.warning("_onLatestDataError, retry again");
  70. // refresh();
  71. Future.delayed(Duration(seconds: 3), () => refresh());
  72. }
  73. }
  74. Future<void> refresh() async {
  75. _log.info('refresh...');
  76. await latestCachedRequest.refresh();
  77. }
  78. void initThird() async {}
  79. ListItem? get currentItem {
  80. if (latest != null && latest!.isNotEmpty) {
  81. return latest![data.currentLevel];
  82. }
  83. return null;
  84. }
  85. @override
  86. Widget build(BuildContext context) {
  87. if (isLoading) return scrollableDummy;
  88. Device device = context.read<Device>();
  89. // 2. 计算画布尺寸(宽=屏幕宽-60,高=宽×3/2)
  90. final canvasWidth = device.screenSize.width - 30 * 2; // 左右各30px
  91. final canvasHeight = canvasWidth * 3 / 2;
  92. return Scaffold(
  93. appBar: AppBar(
  94. backgroundColor: Colors.white,
  95. elevation: 1,
  96. centerTitle: true,
  97. leading: RepaintBoundary(
  98. // !!! 改造点 1: 包裹 RepaintBoundary
  99. key: _collectionKey, // !!! 改造点 2: 关联 GlobalKey
  100. child: IconButton(
  101. onPressed: () {
  102. audio.playSfx(SfxType.tap);
  103. Navigator.push(context, CollectionScreen.buildRoute());
  104. },
  105. icon: const Icon(Icons.collections, color: Colors.black54),
  106. ),
  107. ),
  108. title: const Text(
  109. 'PuzzleWeave',
  110. style: TextStyle(color: Colors.black54, fontFamily: 'Arial Black', fontWeight: FontWeight.bold, fontSize: 24),
  111. ),
  112. actions: [
  113. IconButton(
  114. onPressed: () {
  115. audio.playSfx(SfxType.tap);
  116. Navigator.push(context, SettingsDialog.buildRoute());
  117. },
  118. icon: const Icon(Icons.settings, color: Colors.black54),
  119. ),
  120. ],
  121. ),
  122. body: Column(
  123. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  124. children: [
  125. Expanded(
  126. child: Column(
  127. mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  128. children: [
  129. // 2. 画布区域(固定尺寸)
  130. Padding(
  131. padding: const EdgeInsets.symmetric(horizontal: 30), // 左右30px
  132. child: SizedBox(
  133. width: canvasWidth,
  134. height: canvasHeight,
  135. child: ValueListenableBuilder(
  136. valueListenable: data.completedWorks,
  137. builder: (context, value, child) {
  138. return HomeBoardPlay(
  139. key: _canvasKey,
  140. canvasWidth: canvasWidth,
  141. canvasHeight: canvasHeight,
  142. collectionKey: _collectionKey,
  143. onCollectionDone: () {
  144. // 可选:在这里处理合集解锁后的其他逻辑
  145. },
  146. );
  147. },
  148. ),
  149. ),
  150. ),
  151. playButton,
  152. ],
  153. ),
  154. ),
  155. Container(
  156. height: device.bannerHeight,
  157. width: double.infinity,
  158. color: Colors.green.shade100,
  159. child: const Center(child: Text('Banner 广告区域', style: TextStyle(fontSize: 12))),
  160. ),
  161. ],
  162. ),
  163. );
  164. }
  165. Widget get playButton {
  166. return MyElevatedButton(
  167. width: 200,
  168. height: 70,
  169. borderRadius: BorderRadius.circular(20),
  170. gradient: LinearGradient(colors: [SkinHelper.coreBgColor, SkinHelper.slotBorderColor]),
  171. onPressed: () async {
  172. audio.playSfx(SfxType.tap);
  173. _canvasKey.currentState?.startFlipAnimation();
  174. // if (currentItem != null) {
  175. // PageRouteBuilder? pageRouteBuilder = BoardPlay.buildRoute(currentItem!);
  176. // final result = await Navigator.push(context, pageRouteBuilder);
  177. // if (result == true) {
  178. // _canvasKey.currentState?.startFlipAnimation();
  179. // }
  180. // } else {
  181. // Fluttertoast.showToast(
  182. // msg: "更多图片敬请期待...",
  183. // toastLength: Toast.LENGTH_SHORT,
  184. // gravity: ToastGravity.CENTER,
  185. // timeInSecForIosWeb: 1,
  186. // backgroundColor: SkinHelper.slotBorderColor,
  187. // textColor: Colors.white,
  188. // fontSize: 16.0,
  189. // );
  190. // }
  191. },
  192. child: Column(
  193. mainAxisAlignment: MainAxisAlignment.center,
  194. children: [
  195. const Text(
  196. '玩',
  197. style: TextStyle(color: Colors.white, fontFamily: 'Arial Black', fontSize: 24, fontWeight: FontWeight.bold),
  198. ),
  199. ValueListenableBuilder<List<Work>>(
  200. valueListenable: data.completedWorks,
  201. builder: (context, isSoundOn, child) {
  202. return Text('关卡 ${data.currentLevel + 1}', style: const TextStyle(color: Colors.white, fontSize: 16));
  203. },
  204. ),
  205. ],
  206. ),
  207. );
  208. }
  209. Widget get scrollableDummy => Scaffold(
  210. body: LayoutBuilder(
  211. builder: (p0, p1) {
  212. return SingleChildScrollView(
  213. physics: const AlwaysScrollableScrollPhysics(),
  214. child: SizedBox(
  215. height: p1.maxHeight,
  216. child: Center(child: ListView(shrinkWrap: true, children: [Lottie.asset('assets/lottie/loading.json', height: 100)])),
  217. ),
  218. );
  219. },
  220. ),
  221. );
  222. }