| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- 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<StatefulWidget> createState() => _GalleryScreen();
- }
- const int minimumRemoteLoadCount = 30; // 假设加载到 30 张图才算网络畅通
- class _GalleryScreen extends State<GalleryScreen> {
- late Device device;
- late JcAudioController audio;
- late Data data;
- List<ListItem>? latest;
- late CachedRequest latestCachedRequest;
- late StreamSubscription? latestSubscription;
- @override
- void initState() {
- super.initState();
- device = context.read<Device>();
- audio = context.read<JcAudioController>();
- data = context.read<Data>();
- 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<ListItem>;
- 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<void> refresh() async {
- _log.info('refresh...');
- await latestCachedRequest.refresh();
- }
- @override
- Widget build(BuildContext context) {
- final device = context.read<Device>();
- final isTablet = device.isTablet;
- return Scaffold(
- backgroundColor: SkinHelper.colorWhite,
- body: latest == null
- ? scrollableDummy
- : RefreshIndicator(
- onRefresh: refresh,
- child: CustomScrollView(
- slivers: <Widget>[
- 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<Color>(
- // 使用你定义的 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<bool> 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<void> showCustomTrackingDialog(BuildContext context) async => await showDialog<void>(
- 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>();
- 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);
- }
- }
- }
|