|
|
@@ -0,0 +1,242 @@
|
|
|
+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';
|
|
|
+
|
|
|
+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(
|
|
|
+ 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 _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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|