gallery_screen.dart 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:app_tracking_transparency/app_tracking_transparency.dart';
  4. import 'package:firebase_crashlytics/firebase_crashlytics.dart';
  5. import 'package:firebase_messaging/firebase_messaging.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:logging/logging.dart';
  8. import 'package:lottie/lottie.dart';
  9. import 'package:provider/provider.dart';
  10. import 'package:puzzleweave/ads/applovin_ads_controller.dart';
  11. import 'package:puzzleweave/audio/jc_audio_controller.dart';
  12. import 'package:puzzleweave/config/device.dart';
  13. import 'package:puzzleweave/firebase/adjust_helper.dart';
  14. import 'package:puzzleweave/gallery/grid_item.dart';
  15. import 'package:puzzleweave/models/cached_request.dart';
  16. import 'package:puzzleweave/models/data.dart';
  17. import 'package:puzzleweave/models/items.dart';
  18. import 'package:puzzleweave/persistence/persistence.dart';
  19. import 'package:puzzleweave/skin/skin.dart';
  20. final Logger _log = Logger('gallery_screen');
  21. class GalleryScreen extends StatefulWidget {
  22. const GalleryScreen({super.key});
  23. @override
  24. State<StatefulWidget> createState() => _GalleryScreen();
  25. }
  26. const int minimumRemoteLoadCount = 30; // 假设加载到 30 张图才算网络畅通
  27. class _GalleryScreen extends State<GalleryScreen> {
  28. late Device device;
  29. late JcAudioController audio;
  30. late Data data;
  31. List<ListItem>? latest;
  32. late CachedRequest latestCachedRequest;
  33. late StreamSubscription? latestSubscription;
  34. @override
  35. void initState() {
  36. super.initState();
  37. device = context.read<Device>();
  38. audio = context.read<JcAudioController>();
  39. data = context.read<Data>();
  40. latestCachedRequest = data.latest;
  41. // 主动获取缓存数据(关键)
  42. final cachedData = latestCachedRequest.cachedData;
  43. if (cachedData != null) {
  44. _onLatestDataUpdate(cachedData);
  45. }
  46. latestSubscription = latestCachedRequest.stream.listen(_onLatestDataUpdate, onError: _onLatestDataError);
  47. audio.startMusic();
  48. }
  49. _onLatestDataUpdate(datalist) {
  50. _log.info('_onLatestDataUpdate.... ');
  51. if (datalist != null) {
  52. latest = datalist as List<ListItem>;
  53. setState(() {});
  54. final bool hasSufficientData = datalist.length >= minimumRemoteLoadCount;
  55. if (hasSufficientData) {
  56. // 如果数据完整,无论是否是缓存数据,都尝试初始化第三方服务(因为主页已经可以显示了)
  57. if (!hasInit) {
  58. initThird();
  59. }
  60. }
  61. }
  62. }
  63. _onLatestDataError(error) {
  64. _log.info('_onLatestDataError.... $error');
  65. if (latest == null || latest!.isEmpty || latest!.length < minimumRemoteLoadCount) {
  66. _log.warning("_onLatestDataError, retry again");
  67. // refresh();
  68. Future.delayed(Duration(seconds: 3), () => refresh());
  69. }
  70. }
  71. Future<void> refresh() async {
  72. _log.info('refresh...');
  73. await latestCachedRequest.refresh();
  74. }
  75. @override
  76. Widget build(BuildContext context) {
  77. final device = context.read<Device>();
  78. final isTablet = device.isTablet;
  79. return Scaffold(
  80. backgroundColor: SkinHelper.colorWhite,
  81. body: latest == null
  82. ? scrollableDummy
  83. : RefreshIndicator(
  84. onRefresh: refresh,
  85. child: CustomScrollView(
  86. slivers: <Widget>[
  87. SliverPadding(
  88. sliver: SliverGrid(
  89. gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: isTablet ? 300 : 210, childAspectRatio: 2 / 3),
  90. delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
  91. return _buildItem(context, index);
  92. }, childCount: latest!.length),
  93. ),
  94. padding: const EdgeInsets.only(left: 10.0, right: 10.0),
  95. ),
  96. ],
  97. ),
  98. ),
  99. );
  100. }
  101. // Widget get scrollableDummy => LayoutBuilder(
  102. // builder: (p0, p1) {
  103. // return SingleChildScrollView(
  104. // physics: const AlwaysScrollableScrollPhysics(),
  105. // child: SizedBox(
  106. // height: p1.maxHeight,
  107. // child: Center(
  108. // child: ListView(
  109. // shrinkWrap: true,
  110. // children: [
  111. // Lottie.asset('assets/lottie/loading.json', height: 100),
  112. // const Center(child: Text("loading...")),
  113. // ],
  114. // ),
  115. // ),
  116. // ),
  117. // );
  118. // },
  119. // );
  120. Widget get scrollableDummy => Scaffold(
  121. backgroundColor: SkinHelper.colorWhite, // 确保背景色统一
  122. body: Center(
  123. child: Column(
  124. mainAxisAlignment: MainAxisAlignment.center,
  125. children: [
  126. // 替换 Lottie 为原生轻量级进度条
  127. SizedBox(
  128. width: 40,
  129. height: 40,
  130. child: CircularProgressIndicator(
  131. strokeWidth: 3,
  132. valueColor: AlwaysStoppedAnimation<Color>(
  133. // 使用你定义的 SkinHelper 核心色,如果没有则用黑色
  134. SkinHelper.coreBgColor,
  135. ),
  136. ),
  137. ),
  138. const SizedBox(height: 20),
  139. Text(
  140. "Loading...",
  141. style: TextStyle(color: SkinHelper.slotBorderColor.withOpacity(0.7), fontSize: 14, fontWeight: FontWeight.w500),
  142. ),
  143. ],
  144. ),
  145. ),
  146. );
  147. Widget _buildItem(context, index) {
  148. ListItem item = latest![index];
  149. return Padding(
  150. padding: const EdgeInsets.all(10.0),
  151. child: GridItem(item: item, lock: false, index: index),
  152. );
  153. }
  154. ///////////////////////// 初始化相关 /////////////////////////
  155. static bool hasInit = false;
  156. // 在列表刷出来后才正式初始化admod等组件
  157. void initThird() async {
  158. if (hasInit) return;
  159. hasInit = true;
  160. // 有了UMP后, 这里的ATT就不需要了
  161. // bool auth = await initATT();
  162. // if (auth) {
  163. // await platform.setHasUserConsent(true);
  164. // await platform.setAdvertiserTrackingEnabled(true);
  165. // }
  166. // await initUMP(); // 征询欧洲用户同意 // applovin max 已经可以自动处理,这里不需要了
  167. TrackingStatus attStatus = await AppTrackingTransparency.trackingAuthorizationStatus;
  168. if (attStatus == TrackingStatus.authorized && Platform.isIOS) {
  169. // ATT 通过之后,ios需要调用相关的原生sdk接口做进一步的初始化
  170. // await platform.setHasUserConsent(true);
  171. // await platform.setAdvertiserTrackingEnabled(true);
  172. }
  173. initFCM(); // 消息推送许可弹窗
  174. initAd(); // admod 的广告加载安排在iOS ATT 之后,以便能够加载到个性化广告
  175. AdjustHelper.init(Persistence().uuid); // 初始化Adjust
  176. final idfa = await AppTrackingTransparency.getAdvertisingIdentifier();
  177. _log.info("idfa: $idfa");
  178. }
  179. /////////////////////////// ATT ///////////////////////////
  180. // Platform messages are asynchronous, so we initialize in an async method.
  181. Future<bool> initATT() async {
  182. TrackingStatus status = await AppTrackingTransparency.trackingAuthorizationStatus;
  183. _log.info('initATT111 $status');
  184. // If the system can show an authorization request dialog
  185. if (status == TrackingStatus.notDetermined) {
  186. // Show a custom explainer dialog before the system dialog
  187. // await showCustomTrackingDialog(context);
  188. // Wait for dialog popping animation
  189. // await Future.delayed(const Duration(milliseconds: 200));
  190. // Request system's tracking authorization dialog
  191. status = await AppTrackingTransparency.requestTrackingAuthorization();
  192. _log.info('initATT222 $status');
  193. }
  194. if (status == TrackingStatus.authorized) {
  195. return true;
  196. }
  197. return false;
  198. }
  199. // no need
  200. Future<void> showCustomTrackingDialog(BuildContext context) async => await showDialog<void>(
  201. context: context,
  202. builder: (context) => AlertDialog(
  203. title: const Text('Dear User'),
  204. content: const Text(
  205. 'We care about your privacy and data security. We keep this app free by showing ads. '
  206. 'Can we continue to use your data to tailor ads for you?\n\nYou can change your choice anytime in the app settings. '
  207. 'Our partners will collect data and use a unique identifier on your device to show you ads.',
  208. ),
  209. actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('Continue'))],
  210. ),
  211. );
  212. /////////////////////////////////////////////////////////
  213. ///
  214. /// 初始化广告模块
  215. initAd() {
  216. _log.info('initAd');
  217. ApplovinAdsController applovinAdsController = context.read<ApplovinAdsController>();
  218. applovinAdsController.initialize();
  219. }
  220. /////////////////////////// FCM ///////////////////////////
  221. // 消息推送许可弹框
  222. initFCM() async {
  223. try {
  224. final fcmToken = await FirebaseMessaging.instance.getToken();
  225. _log.info("FCM Token: $fcmToken");
  226. FirebaseMessaging messaging = FirebaseMessaging.instance;
  227. NotificationSettings settings = await messaging.requestPermission(
  228. alert: true,
  229. announcement: false,
  230. badge: true,
  231. carPlay: false,
  232. criticalAlert: false,
  233. provisional: false,
  234. sound: true,
  235. );
  236. _log.warning('User granted permission: ${settings.authorizationStatus}');
  237. } catch (e) {
  238. FirebaseCrashlytics.instance.log("FCM FirebaseMessaging.instance.getToken error: $e");
  239. _log.warning(e);
  240. }
  241. }
  242. }