import 'dart:async'; import 'dart:io'; import 'package:app_tracking_transparency/app_tracking_transparency.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:lottie/lottie.dart'; import 'package:provider/provider.dart'; import 'package:puzzleweave/ads/applovin_ads_controller.dart'; import 'package:puzzleweave/audio/jc_audio_controller.dart'; import 'package:puzzleweave/config/device.dart'; import 'package:puzzleweave/firebase/adjust_helper.dart'; import 'package:puzzleweave/gallery/grid_item.dart'; import 'package:puzzleweave/models/cached_request.dart'; import 'package:puzzleweave/models/data.dart'; import 'package:puzzleweave/models/items.dart'; import 'package:puzzleweave/persistence/persistence.dart'; import 'package:puzzleweave/skin/skin.dart'; final Logger _log = Logger('gallery_screen'); class GalleryScreen extends StatefulWidget { const GalleryScreen({super.key}); @override State createState() => _GalleryScreen(); } const int minimumRemoteLoadCount = 30; // 假设加载到 30 张图才算网络畅通 class _GalleryScreen extends State { late Device device; late JcAudioController audio; late Data data; List? latest; late CachedRequest latestCachedRequest; late StreamSubscription? latestSubscription; @override void initState() { super.initState(); device = context.read(); audio = context.read(); data = context.read(); latestCachedRequest = data.latest; // 主动获取缓存数据(关键) final cachedData = latestCachedRequest.cachedData; if (cachedData != null) { _onLatestDataUpdate(cachedData); } latestSubscription = latestCachedRequest.stream.listen(_onLatestDataUpdate, onError: _onLatestDataError); audio.startMusic(); } _onLatestDataUpdate(datalist) { _log.info('_onLatestDataUpdate.... '); if (datalist != null) { latest = datalist as List; setState(() {}); final bool hasSufficientData = datalist.length >= minimumRemoteLoadCount; if (hasSufficientData) { // 如果数据完整,无论是否是缓存数据,都尝试初始化第三方服务(因为主页已经可以显示了) if (!hasInit) { initThird(); } } } } _onLatestDataError(error) { _log.info('_onLatestDataError.... $error'); if (latest == null || latest!.isEmpty || latest!.length < minimumRemoteLoadCount) { _log.warning("_onLatestDataError, retry again"); // refresh(); Future.delayed(Duration(seconds: 3), () => refresh()); } } Future refresh() async { _log.info('refresh...'); await latestCachedRequest.refresh(); } @override Widget build(BuildContext context) { final device = context.read(); final isTablet = device.isTablet; return Scaffold( backgroundColor: SkinHelper.colorWhite, body: latest == null ? scrollableDummy : RefreshIndicator( onRefresh: refresh, child: CustomScrollView( slivers: [ SliverPadding( sliver: SliverGrid( gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: isTablet ? 300 : 210, childAspectRatio: 2 / 3), delegate: SliverChildBuilderDelegate((BuildContext context, int index) { return _buildItem(context, index); }, childCount: latest!.length), ), padding: const EdgeInsets.only(left: 10.0, right: 10.0), ), ], ), ), ); } // Widget get scrollableDummy => LayoutBuilder( // builder: (p0, p1) { // return SingleChildScrollView( // physics: const AlwaysScrollableScrollPhysics(), // child: SizedBox( // height: p1.maxHeight, // child: Center( // child: ListView( // shrinkWrap: true, // children: [ // Lottie.asset('assets/lottie/loading.json', height: 100), // const Center(child: Text("loading...")), // ], // ), // ), // ), // ); // }, // ); Widget get scrollableDummy => Scaffold( backgroundColor: SkinHelper.colorWhite, // 确保背景色统一 body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // 替换 Lottie 为原生轻量级进度条 SizedBox( width: 40, height: 40, child: CircularProgressIndicator( strokeWidth: 3, valueColor: AlwaysStoppedAnimation( // 使用你定义的 SkinHelper 核心色,如果没有则用黑色 SkinHelper.coreBgColor, ), ), ), const SizedBox(height: 20), Text( "Loading...", style: TextStyle(color: SkinHelper.slotBorderColor.withOpacity(0.7), fontSize: 14, fontWeight: FontWeight.w500), ), ], ), ), ); Widget _buildItem(context, index) { ListItem item = latest![index]; return Padding( padding: const EdgeInsets.all(10.0), child: GridItem(item: item, lock: false, index: index), ); } ///////////////////////// 初始化相关 ///////////////////////// static bool hasInit = false; // 在列表刷出来后才正式初始化admod等组件 void initThird() async { if (hasInit) return; hasInit = true; // 有了UMP后, 这里的ATT就不需要了 // bool auth = await initATT(); // if (auth) { // await platform.setHasUserConsent(true); // await platform.setAdvertiserTrackingEnabled(true); // } // await initUMP(); // 征询欧洲用户同意 // applovin max 已经可以自动处理,这里不需要了 TrackingStatus attStatus = await AppTrackingTransparency.trackingAuthorizationStatus; if (attStatus == TrackingStatus.authorized && Platform.isIOS) { // ATT 通过之后,ios需要调用相关的原生sdk接口做进一步的初始化 // await platform.setHasUserConsent(true); // await platform.setAdvertiserTrackingEnabled(true); } initFCM(); // 消息推送许可弹窗 initAd(); // admod 的广告加载安排在iOS ATT 之后,以便能够加载到个性化广告 AdjustHelper.init(Persistence().uuid); // 初始化Adjust final idfa = await AppTrackingTransparency.getAdvertisingIdentifier(); _log.info("idfa: $idfa"); } /////////////////////////// ATT /////////////////////////// // Platform messages are asynchronous, so we initialize in an async method. Future initATT() async { TrackingStatus status = await AppTrackingTransparency.trackingAuthorizationStatus; _log.info('initATT111 $status'); // If the system can show an authorization request dialog if (status == TrackingStatus.notDetermined) { // Show a custom explainer dialog before the system dialog // await showCustomTrackingDialog(context); // Wait for dialog popping animation // await Future.delayed(const Duration(milliseconds: 200)); // Request system's tracking authorization dialog status = await AppTrackingTransparency.requestTrackingAuthorization(); _log.info('initATT222 $status'); } if (status == TrackingStatus.authorized) { return true; } return false; } // no need Future showCustomTrackingDialog(BuildContext context) async => await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Dear User'), content: const Text( 'We care about your privacy and data security. We keep this app free by showing ads. ' 'Can we continue to use your data to tailor ads for you?\n\nYou can change your choice anytime in the app settings. ' 'Our partners will collect data and use a unique identifier on your device to show you ads.', ), actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('Continue'))], ), ); ///////////////////////////////////////////////////////// /// /// 初始化广告模块 initAd() { _log.info('initAd'); ApplovinAdsController applovinAdsController = context.read(); applovinAdsController.initialize(); } /////////////////////////// FCM /////////////////////////// // 消息推送许可弹框 initFCM() async { try { final fcmToken = await FirebaseMessaging.instance.getToken(); _log.info("FCM Token: $fcmToken"); FirebaseMessaging messaging = FirebaseMessaging.instance; NotificationSettings settings = await messaging.requestPermission( alert: true, announcement: false, badge: true, carPlay: false, criticalAlert: false, provisional: false, sound: true, ); _log.warning('User granted permission: ${settings.authorizationStatus}'); } catch (e) { FirebaseCrashlytics.instance.log("FCM FirebaseMessaging.instance.getToken error: $e"); _log.warning(e); } } }