|
@@ -3,14 +3,12 @@ import 'dart:io';
|
|
|
import 'dart:math';
|
|
import 'dart:math';
|
|
|
|
|
|
|
|
import 'package:app_tracking_transparency/app_tracking_transparency.dart';
|
|
import 'package:app_tracking_transparency/app_tracking_transparency.dart';
|
|
|
-import 'package:applovin_max/applovin_max.dart';
|
|
|
|
|
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
|
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
|
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
import 'package:flutter_svg/svg.dart';
|
|
import 'package:flutter_svg/svg.dart';
|
|
|
import 'package:fluttertoast/fluttertoast.dart';
|
|
import 'package:fluttertoast/fluttertoast.dart';
|
|
|
import 'package:logging/logging.dart';
|
|
import 'package:logging/logging.dart';
|
|
|
-import 'package:lottie/lottie.dart';
|
|
|
|
|
import 'package:provider/provider.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
import 'package:puzzleweave/ads/applovin_ads_controller.dart';
|
|
import 'package:puzzleweave/ads/applovin_ads_controller.dart';
|
|
|
import 'package:puzzleweave/audio/jc_audio_controller.dart';
|
|
import 'package:puzzleweave/audio/jc_audio_controller.dart';
|
|
@@ -21,18 +19,18 @@ import 'package:puzzleweave/firebase/adjust_helper.dart';
|
|
|
import 'package:puzzleweave/homepage/home_board_play.dart';
|
|
import 'package:puzzleweave/homepage/home_board_play.dart';
|
|
|
import 'package:puzzleweave/l10n/app_localizations.dart';
|
|
import 'package:puzzleweave/l10n/app_localizations.dart';
|
|
|
import 'package:puzzleweave/models/cached_request.dart';
|
|
import 'package:puzzleweave/models/cached_request.dart';
|
|
|
-import 'package:puzzleweave/models/data.dart';
|
|
|
|
|
import 'package:puzzleweave/models/download.dart';
|
|
import 'package:puzzleweave/models/download.dart';
|
|
|
import 'package:puzzleweave/models/items.dart';
|
|
import 'package:puzzleweave/models/items.dart';
|
|
|
import 'package:puzzleweave/persistence/persistence.dart';
|
|
import 'package:puzzleweave/persistence/persistence.dart';
|
|
|
-import 'package:puzzleweave/platform/my_method_channel.dart';
|
|
|
|
|
import 'package:puzzleweave/play/board_play.dart';
|
|
import 'package:puzzleweave/play/board_play.dart';
|
|
|
import 'package:puzzleweave/settings/settings_screen.dart';
|
|
import 'package:puzzleweave/settings/settings_screen.dart';
|
|
|
import 'package:puzzleweave/skin/skin.dart';
|
|
import 'package:puzzleweave/skin/skin.dart';
|
|
|
import 'package:puzzleweave/utils/mybutton.dart';
|
|
import 'package:puzzleweave/utils/mybutton.dart';
|
|
|
import 'package:puzzleweave/utils/utils.dart';
|
|
import 'package:puzzleweave/utils/utils.dart';
|
|
|
|
|
|
|
|
|
|
+import '../ads/ad_helper.dart';
|
|
|
import '../ads/ads_state.dart';
|
|
import '../ads/ads_state.dart';
|
|
|
|
|
+import '../utils/memory_monitor.dart';
|
|
|
|
|
|
|
|
final Logger _log = Logger('home_screen');
|
|
final Logger _log = Logger('home_screen');
|
|
|
|
|
|
|
@@ -43,77 +41,113 @@ class HomeScreen extends StatefulWidget {
|
|
|
State<StatefulWidget> createState() => _HomeScreen();
|
|
State<StatefulWidget> createState() => _HomeScreen();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const int minimumRemoteLoadCount = 30; // 假设加载到 30 张图才算网络畅通
|
|
|
|
|
|
|
+const int minimumRemoteLoadCount = 30;
|
|
|
|
|
|
|
|
class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
|
|
class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
|
|
|
late Device device;
|
|
late Device device;
|
|
|
late JcAudioController audio;
|
|
late JcAudioController audio;
|
|
|
- late Data data;
|
|
|
|
|
List<ListItem>? latest;
|
|
List<ListItem>? latest;
|
|
|
late CachedRequest latestCachedRequest;
|
|
late CachedRequest latestCachedRequest;
|
|
|
late StreamSubscription? latestSubscription;
|
|
late StreamSubscription? latestSubscription;
|
|
|
|
|
|
|
|
- // 自定义画布控制器(可选,用于控制画布绘制逻辑)
|
|
|
|
|
final _canvasKey = GlobalKey<HomeBoardPlayState>();
|
|
final _canvasKey = GlobalKey<HomeBoardPlayState>();
|
|
|
- // !!! 新增:用于定位 Collection 按钮的 GlobalKey
|
|
|
|
|
final GlobalKey _collectionKey = GlobalKey();
|
|
final GlobalKey _collectionKey = GlobalKey();
|
|
|
|
|
|
|
|
bool isLoading = true;
|
|
bool isLoading = true;
|
|
|
|
|
+ bool firstRun = false;
|
|
|
|
|
|
|
|
- // !!! 新增:Collection 按钮的动画控制器和动画
|
|
|
|
|
- late AnimationController _collectionController; // 左上角 collection button 的动画控制器
|
|
|
|
|
- late Animation<double> _collectionAnimation; // 放大/缩小动画
|
|
|
|
|
|
|
+ // ✅ 优化点1: 导航状态管理
|
|
|
|
|
+ bool _isNavigating = false;
|
|
|
|
|
|
|
|
- bool firstRun = false;
|
|
|
|
|
|
|
+ // ✅ 优化点2: 防抖机制
|
|
|
|
|
+ Timer? _refreshDebouncer;
|
|
|
|
|
+
|
|
|
|
|
+ // ✅ 优化点3: 缓存计算结果
|
|
|
|
|
+ double? _cachedCanvasWidth;
|
|
|
|
|
+ double? _cachedCanvasHeight;
|
|
|
|
|
+ bool _layoutCalculated = false;
|
|
|
|
|
+
|
|
|
|
|
+ late AnimationController _collectionController;
|
|
|
|
|
+ late Animation<double> _collectionAnimation;
|
|
|
|
|
+
|
|
|
|
|
+ bool interPending = false;
|
|
|
|
|
|
|
|
@override
|
|
@override
|
|
|
void initState() {
|
|
void initState() {
|
|
|
super.initState();
|
|
super.initState();
|
|
|
-
|
|
|
|
|
_log.info("首页初始化");
|
|
_log.info("首页初始化");
|
|
|
|
|
|
|
|
- // 在组件绘制后检查 firstRun 并导航
|
|
|
|
|
- if (Persistence().firstRun) {
|
|
|
|
|
- firstRun = true;
|
|
|
|
|
- WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
- // 仅当未跳转过时执行
|
|
|
|
|
- _handleFirstRunNavigation();
|
|
|
|
|
- });
|
|
|
|
|
- Persistence().firstRun = false;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ _initializeComponents();
|
|
|
|
|
+ _setupAnimations();
|
|
|
|
|
+ _handleInitialNavigation();
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ void _initializeComponents() {
|
|
|
device = context.read<Device>();
|
|
device = context.read<Device>();
|
|
|
audio = context.read<JcAudioController>();
|
|
audio = context.read<JcAudioController>();
|
|
|
- data = context.read<Data>();
|
|
|
|
|
latestCachedRequest = data.latest;
|
|
latestCachedRequest = data.latest;
|
|
|
- // 主动获取缓存数据(关键)
|
|
|
|
|
|
|
+
|
|
|
final cachedData = latestCachedRequest.cachedData;
|
|
final cachedData = latestCachedRequest.cachedData;
|
|
|
if (cachedData != null) {
|
|
if (cachedData != null) {
|
|
|
_onLatestDataUpdate(cachedData);
|
|
_onLatestDataUpdate(cachedData);
|
|
|
}
|
|
}
|
|
|
latestSubscription = latestCachedRequest.stream.listen(_onLatestDataUpdate, onError: _onLatestDataError);
|
|
latestSubscription = latestCachedRequest.stream.listen(_onLatestDataUpdate, onError: _onLatestDataError);
|
|
|
|
|
|
|
|
- // !!! 改造点 1: 初始化 Collection 按钮动画
|
|
|
|
|
- _collectionController =
|
|
|
|
|
- AnimationController(
|
|
|
|
|
- // 设定总时长
|
|
|
|
|
- duration: const Duration(milliseconds: 300),
|
|
|
|
|
- vsync: this,
|
|
|
|
|
- )..addStatusListener((status) {
|
|
|
|
|
- if (status == AnimationStatus.completed) {
|
|
|
|
|
- audio.playSfx(SfxType.pop);
|
|
|
|
|
|
|
+ onInterstitialAdState = _createInterStateListener();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Function(AdState state) _createInterStateListener() {
|
|
|
|
|
+ return (AdState state) {
|
|
|
|
|
+ _log.info('Interstitial ad state changed: $state');
|
|
|
|
|
+ if (state == AdState.dismissed && interPending) {
|
|
|
|
|
+ _log.info('Interstitial ad dismissed, executing pending post-ad logic.');
|
|
|
|
|
+ _canvasKey.currentState?.startFlipAnimation();
|
|
|
|
|
+
|
|
|
|
|
+ final bool hasSufficientData = latest != null && latest!.length >= minimumRemoteLoadCount;
|
|
|
|
|
+ final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch;
|
|
|
|
|
+
|
|
|
|
|
+ if (hasSufficientData) {
|
|
|
|
|
+ if (isNetworkActive) {
|
|
|
|
|
+ _log.info('Game finished, data complete & Network Active. Triggering sequential preloading...');
|
|
|
|
|
+ _preloadNextImages();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ _log.info('Game finished, data complete but Network inactive. Attempting refresh.');
|
|
|
|
|
+ refresh();
|
|
|
}
|
|
}
|
|
|
- });
|
|
|
|
|
|
|
+ } else {
|
|
|
|
|
+ _log.info('Game finished, remote data incomplete. Attempting refresh...');
|
|
|
|
|
+ refresh();
|
|
|
|
|
+ }
|
|
|
|
|
+ interPending = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void _setupAnimations() {
|
|
|
|
|
+ _collectionController = AnimationController(duration: const Duration(milliseconds: 300), vsync: this)
|
|
|
|
|
+ ..addStatusListener((status) {
|
|
|
|
|
+ if (status == AnimationStatus.completed) {
|
|
|
|
|
+ audio.playSfx(SfxType.pop);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
- // !!! 改造点 2: 使用 TweenSequence 实现平滑的放大和缩小
|
|
|
|
|
_collectionAnimation = TweenSequence<double>([
|
|
_collectionAnimation = TweenSequence<double>([
|
|
|
- // 阶段 1: 放大到 1.3 (占总时长的 50%)
|
|
|
|
|
TweenSequenceItem(tween: Tween<double>(begin: 1.0, end: 1.4).chain(CurveTween(curve: Curves.easeOut)), weight: 40.0),
|
|
TweenSequenceItem(tween: Tween<double>(begin: 1.0, end: 1.4).chain(CurveTween(curve: Curves.easeOut)), weight: 40.0),
|
|
|
- // 阶段 2: 缩小回 1.0 (占总时长的 50%)
|
|
|
|
|
TweenSequenceItem(tween: Tween<double>(begin: 1.4, end: 1.0).chain(CurveTween(curve: Curves.easeIn)), weight: 60.0),
|
|
TweenSequenceItem(tween: Tween<double>(begin: 1.4, end: 1.0).chain(CurveTween(curve: Curves.easeIn)), weight: 60.0),
|
|
|
]).animate(_collectionController);
|
|
]).animate(_collectionController);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void _handleInitialNavigation() {
|
|
|
|
|
+ if (Persistence().firstRun) {
|
|
|
|
|
+ firstRun = true;
|
|
|
|
|
+ WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
+ if (mounted && !_isNavigating) {
|
|
|
|
|
+ _handleFirstRunNavigation();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ Persistence().firstRun = false;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 只有在应用是 resumed 状态且当前页面在前台时才自动播
|
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed && mounted) {
|
|
if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed && mounted) {
|
|
|
audio.startMusic();
|
|
audio.startMusic();
|
|
@@ -121,8 +155,10 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 首页初始化之后的跳转,首次运行直接进入play页面,上次从play页面退出有缓存存在也跳转到play页面
|
|
|
|
|
- void _handleFirstRunNavigation() async {
|
|
|
|
|
|
|
+ // ✅ 优化点4: 异步化首次运行导航
|
|
|
|
|
+ void _handleFirstRunNavigation() {
|
|
|
|
|
+ if (_isNavigating) return;
|
|
|
|
|
+
|
|
|
_log.info('First run detected, navigating to initial play page.');
|
|
_log.info('First run detected, navigating to initial play page.');
|
|
|
final AssetItem initialItem = AssetItem(
|
|
final AssetItem initialItem = AssetItem(
|
|
|
Config.firstId,
|
|
Config.firstId,
|
|
@@ -137,222 +173,198 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
|
|
|
return gotoPlay(initialItem, firstRun: true);
|
|
return gotoPlay(initialItem, firstRun: true);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 检查是否需要跳转到boardplay
|
|
|
|
|
- void checkGoPlay() async {
|
|
|
|
|
- if (currentItem != null) {
|
|
|
|
|
- final jsonFile = await localFile(currentItem!.jsonPath);
|
|
|
|
|
- final exists = await jsonFile.exists();
|
|
|
|
|
|
|
+ // ✅ 优化点5: 异步化文件检查
|
|
|
|
|
+ void checkGoPlay() {
|
|
|
|
|
+ if (currentItem == null || _isNavigating) return;
|
|
|
|
|
|
|
|
- // !!! 关键修复:检查当前组件是否还在组件树中
|
|
|
|
|
- if (!mounted) return;
|
|
|
|
|
|
|
+ Future.microtask(() async {
|
|
|
|
|
+ try {
|
|
|
|
|
+ final jsonFile = await localFile(currentItem!.jsonPath);
|
|
|
|
|
+ final exists = await jsonFile.exists();
|
|
|
|
|
|
|
|
- if (exists) {
|
|
|
|
|
- gotoPlay(currentItem!);
|
|
|
|
|
|
|
+ if (mounted && exists && !_isNavigating) {
|
|
|
|
|
+ gotoPlay(currentItem!);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ _log.warning('Error checking play file: $e');
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
@override
|
|
|
void dispose() {
|
|
void dispose() {
|
|
|
|
|
+ _refreshDebouncer?.cancel();
|
|
|
latestSubscription?.cancel();
|
|
latestSubscription?.cancel();
|
|
|
_collectionController.dispose();
|
|
_collectionController.dispose();
|
|
|
super.dispose();
|
|
super.dispose();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ✅ 优化点6: 简化数据更新逻辑
|
|
|
_onLatestDataUpdate(datalist) {
|
|
_onLatestDataUpdate(datalist) {
|
|
|
- _log.info('_onLatestDataUpdate.... ');
|
|
|
|
|
- if (datalist != null) {
|
|
|
|
|
- bool check = false;
|
|
|
|
|
- if (currentItem == null && datalist != null && !firstRun) {
|
|
|
|
|
- check = true;
|
|
|
|
|
- }
|
|
|
|
|
- latest = datalist as List<ListItem>;
|
|
|
|
|
- isLoading = false;
|
|
|
|
|
- setState(() {});
|
|
|
|
|
|
|
+ _log.info('_onLatestDataUpdate....');
|
|
|
|
|
+ if (datalist == null) return;
|
|
|
|
|
|
|
|
- // 1. 检查数据量是否达到最低要求 (>= 30)
|
|
|
|
|
- final bool hasSufficientData = datalist.length >= minimumRemoteLoadCount;
|
|
|
|
|
|
|
+ bool shouldCheckGoPlay = false;
|
|
|
|
|
+ if (currentItem == null && !firstRun) {
|
|
|
|
|
+ shouldCheckGoPlay = true;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 2. 检查数据是否来自最近一次成功的网络请求
|
|
|
|
|
- final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch; // !!! 关键检查点
|
|
|
|
|
|
|
+ latest = datalist as List<ListItem>;
|
|
|
|
|
+ isLoading = false;
|
|
|
|
|
+ setState(() {});
|
|
|
|
|
|
|
|
- if (hasSufficientData) {
|
|
|
|
|
- // 如果数据完整,无论是否是缓存数据,都尝试初始化第三方服务(因为主页已经可以显示了)
|
|
|
|
|
- if (!hasInit) {
|
|
|
|
|
- initThird();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ final bool hasSufficientData = datalist.length >= minimumRemoteLoadCount;
|
|
|
|
|
+ final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch;
|
|
|
|
|
|
|
|
- if (!isNetworkActive) {
|
|
|
|
|
- // 如果是从缓存读取的,网络状态未知,静默刷新列表即可,不触发图片下载
|
|
|
|
|
- Future.delayed(Duration(seconds: 3), () => refresh());
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- // 数据不足 (例如,只有内置图),无论是缓存还是远程失败,都需要重试
|
|
|
|
|
- _log.info('Data insufficient (only ${datalist.length} items). Attempting refresh in 3s.');
|
|
|
|
|
- Future.delayed(Duration(seconds: 3), () => refresh());
|
|
|
|
|
|
|
+ if (hasSufficientData) {
|
|
|
|
|
+ if (!hasInit) {
|
|
|
|
|
+ initThird();
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- if (check) {
|
|
|
|
|
- checkGoPlay();
|
|
|
|
|
|
|
+ if (!isNetworkActive) {
|
|
|
|
|
+ _debouncedRefresh();
|
|
|
}
|
|
}
|
|
|
|
|
+ } else {
|
|
|
|
|
+ _log.info('Data insufficient (only ${datalist.length} items). Attempting refresh.');
|
|
|
|
|
+ _debouncedRefresh();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (shouldCheckGoPlay) {
|
|
|
|
|
+ checkGoPlay();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
_onLatestDataError(error) {
|
|
_onLatestDataError(error) {
|
|
|
_log.info('_onLatestDataError.... $error');
|
|
_log.info('_onLatestDataError.... $error');
|
|
|
if (latest == null || latest!.isEmpty || latest!.length < 20) {
|
|
if (latest == null || latest!.isEmpty || latest!.length < 20) {
|
|
|
- // 列表数据如果少于20,说明只是内置图,仍然刷新远程请求
|
|
|
|
|
_log.warning("_onLatestDataError, retry again");
|
|
_log.warning("_onLatestDataError, retry again");
|
|
|
- // refresh();
|
|
|
|
|
- Future.delayed(Duration(seconds: 3), () => refresh());
|
|
|
|
|
|
|
+ _debouncedRefresh();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ✅ 优化点7: 防抖刷新
|
|
|
|
|
+ void _debouncedRefresh() {
|
|
|
|
|
+ _refreshDebouncer?.cancel();
|
|
|
|
|
+ _refreshDebouncer = Timer(const Duration(seconds: 2), () {
|
|
|
|
|
+ if (mounted) {
|
|
|
|
|
+ refresh();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
Future<void> refresh() async {
|
|
Future<void> refresh() async {
|
|
|
_log.info('refresh...');
|
|
_log.info('refresh...');
|
|
|
await latestCachedRequest.refresh();
|
|
await latestCachedRequest.refresh();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // ListItem? get currentItem {
|
|
|
|
|
- // if (latest != null && latest!.isNotEmpty && data.currentLevel < latest!.length) {
|
|
|
|
|
- // // return latest![data.currentLevel]; // 原来的逻辑,太过简单,如果后台图片有调整顺序变了,用户可能会遇到重复的图
|
|
|
|
|
- // // todo... 改成从latest列表中查找首个 data.completedWorks 中不存在的图(即首个未完成图)
|
|
|
|
|
- // }
|
|
|
|
|
- // return null;
|
|
|
|
|
- // }
|
|
|
|
|
-
|
|
|
|
|
ListItem? get currentItem {
|
|
ListItem? get currentItem {
|
|
|
- // 1. 确保 latest 数据已加载
|
|
|
|
|
if (latest == null || latest!.isEmpty) {
|
|
if (latest == null || latest!.isEmpty) {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. 获取已完成作品的唯一标识符集合,方便快速查找
|
|
|
|
|
- // 假设 ListItem 的 id/url/name 等属性是其唯一标识。
|
|
|
|
|
- // 我们使用 id 作为唯一标识符。
|
|
|
|
|
final Set<String> completedIds = data.completedWorks.value.map((work) => work.id).toSet();
|
|
final Set<String> completedIds = data.completedWorks.value.map((work) => work.id).toSet();
|
|
|
|
|
|
|
|
- // 3. 遍历 latest 列表,查找第一个未完成的 Item
|
|
|
|
|
for (final item in latest!) {
|
|
for (final item in latest!) {
|
|
|
- // 假设 ListItem 有一个唯一的 id 属性。
|
|
|
|
|
- // 如果 ListItem 没有 id,您需要使用其 URL 或其他唯一标识。
|
|
|
|
|
- // 这里我们假设 ListItem 是 RemoteItem/AssetItem 的基类,它们有一个 String 类型的 id 属性。
|
|
|
|
|
final String itemId = item.id;
|
|
final String itemId = item.id;
|
|
|
-
|
|
|
|
|
- // 检查这个 id 是否在已完成集合中
|
|
|
|
|
if (!completedIds.contains(itemId)) {
|
|
if (!completedIds.contains(itemId)) {
|
|
|
_log.info('Found current item: $itemId');
|
|
_log.info('Found current item: $itemId');
|
|
|
- return item; // 返回找到的第一个未完成的 Item
|
|
|
|
|
|
|
+ return item;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 4. 如果所有图片都完成了
|
|
|
|
|
_log.info('All items in the latest list have been completed.');
|
|
_log.info('All items in the latest list have been completed.');
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /// 预加载未来 N 张图片到磁盘,并最后触发当前关卡下载以最大化内存缓存命中率。
|
|
|
|
|
- void _preloadNextImages() async {
|
|
|
|
|
- // 预加载数量 (包括当前关卡在内,共 20 个)
|
|
|
|
|
- const int totalPreloadCount = 20;
|
|
|
|
|
|
|
+ // ✅ 优化点8: 后台预加载
|
|
|
|
|
+ void _preloadNextImages() {
|
|
|
|
|
+ Future.microtask(() async {
|
|
|
|
|
+ const int totalPreloadCount = 20;
|
|
|
|
|
|
|
|
- // 1. 确保 latest 数据已加载
|
|
|
|
|
- if (latest == null || latest!.isEmpty || latest!.length < minimumRemoteLoadCount) {
|
|
|
|
|
- _log.info('Preload failed: latest list is empty.');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (latest == null || latest!.isEmpty || latest!.length < minimumRemoteLoadCount) {
|
|
|
|
|
+ _log.info('Preload failed: latest list is empty.');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 2. 查找当前未完成的第一张图片的索引 (Index of currentItem)
|
|
|
|
|
- final Set<String> completedIds = data.completedWorks.value.map((work) => work.id).toSet();
|
|
|
|
|
- int startIndex = -1;
|
|
|
|
|
- for (int i = 0; i < latest!.length; i++) {
|
|
|
|
|
- if (!completedIds.contains(latest![i].id)) {
|
|
|
|
|
- startIndex = i;
|
|
|
|
|
- break;
|
|
|
|
|
|
|
+ final Set<String> completedIds = data.completedWorks.value.map((work) => work.id).toSet();
|
|
|
|
|
+ int startIndex = -1;
|
|
|
|
|
+ for (int i = 0; i < latest!.length; i++) {
|
|
|
|
|
+ if (!completedIds.contains(latest![i].id)) {
|
|
|
|
|
+ startIndex = i;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- if (startIndex == -1) {
|
|
|
|
|
- _log.info('Preload: All images completed, nothing to preload.');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (startIndex == -1) {
|
|
|
|
|
+ _log.info('Preload: All images completed, nothing to preload.');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 确定预加载范围 (从当前图片startIndex到 totalPreloadCount 个图片)
|
|
|
|
|
- final int endPreloadIndex = min(startIndex + totalPreloadCount, latest!.length);
|
|
|
|
|
|
|
+ final int endPreloadIndex = min(startIndex + totalPreloadCount, latest!.length);
|
|
|
|
|
+ final List<ListItem> itemsToLoad = latest!.sublist(startIndex, endPreloadIndex);
|
|
|
|
|
|
|
|
- // 3. 准备要加载的列表 (从 startIndex 开始)
|
|
|
|
|
- final List<ListItem> itemsToLoad = latest!.sublist(startIndex, endPreloadIndex);
|
|
|
|
|
|
|
+ if (itemsToLoad.isEmpty) {
|
|
|
|
|
+ _log.info('Preload: No items found in the range.');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (itemsToLoad.isEmpty) {
|
|
|
|
|
- _log.info('Preload: No items found in the range.');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ final ListItem currentItemToLoad = itemsToLoad.removeAt(0);
|
|
|
|
|
+ itemsToLoad.add(currentItemToLoad);
|
|
|
|
|
|
|
|
- // 4. 将当前关卡 (第一个元素) 移动到列表的末尾
|
|
|
|
|
- final ListItem currentItemToLoad = itemsToLoad.removeAt(0);
|
|
|
|
|
- itemsToLoad.add(currentItemToLoad);
|
|
|
|
|
-
|
|
|
|
|
- _log.info('Preloading ${itemsToLoad.length} images. Current item: ${currentItemToLoad.id} will be loaded last.');
|
|
|
|
|
-
|
|
|
|
|
- // 5. 循环触发 ItemLoader 加载
|
|
|
|
|
- int preloadCount = 0;
|
|
|
|
|
- for (final item in itemsToLoad) {
|
|
|
|
|
- // 对远程图片进行预加载
|
|
|
|
|
- // 我们不关心返回值或 Future,只是触发下载
|
|
|
|
|
- if (item is RemoteItem) {
|
|
|
|
|
- try {
|
|
|
|
|
- // 使用静态 preload 方法,不再创建复杂的 Loader 实例
|
|
|
|
|
- ItemLoader.preload(item, device.suggestedQuality);
|
|
|
|
|
- // 稍微给一点延迟,避免瞬时并发 I/O 导致 UI 顿挫
|
|
|
|
|
- await Future.delayed(const Duration(milliseconds: 100));
|
|
|
|
|
- preloadCount++;
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- _log.warning('Failed to load item for preloading: ${item.id}, error: $e');
|
|
|
|
|
|
|
+ _log.info('Preloading ${itemsToLoad.length} images. Current item: ${currentItemToLoad.id} will be loaded last.');
|
|
|
|
|
+
|
|
|
|
|
+ int preloadCount = 0;
|
|
|
|
|
+ for (final item in itemsToLoad) {
|
|
|
|
|
+ if (item is RemoteItem) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ ItemLoader.preload(item, device.suggestedQuality);
|
|
|
|
|
+ await Future.delayed(const Duration(milliseconds: 100));
|
|
|
|
|
+ preloadCount++;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ _log.warning('Failed to load item for preloading: ${item.id}, error: $e');
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- _log.info('Preload initiated for $preloadCount remote images, current item was last.');
|
|
|
|
|
|
|
+ _log.info('Preload initiated for $preloadCount remote images, current item was last.');
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- @override
|
|
|
|
|
- Widget build(BuildContext context) {
|
|
|
|
|
- if (isLoading) return scrollableDummy;
|
|
|
|
|
-
|
|
|
|
|
- // 2. 计算画布尺寸(宽=屏幕宽-60,高=宽×3/2)
|
|
|
|
|
- // final canvasWidth = device.screenSize.width - 30 * 2; // 左右各30px
|
|
|
|
|
- // final canvasHeight = canvasWidth * 3 / 2;
|
|
|
|
|
|
|
+ // ✅ 优化点9: 缓存布局计算
|
|
|
|
|
+ void _calculateLayout() {
|
|
|
|
|
+ if (_layoutCalculated) return;
|
|
|
|
|
|
|
|
final double availableHeight = device.screenSize.height - device.appBarHeight - device.bannerHeight - 120;
|
|
final double availableHeight = device.screenSize.height - device.appBarHeight - device.bannerHeight - 120;
|
|
|
-
|
|
|
|
|
- final double paddedWidth = device.screenSize.width - 2 * 30; // padding width 30
|
|
|
|
|
|
|
+ final double paddedWidth = device.screenSize.width - 2 * 30;
|
|
|
final double paddedHeight = availableHeight;
|
|
final double paddedHeight = availableHeight;
|
|
|
-
|
|
|
|
|
final double targetWidth = paddedWidth;
|
|
final double targetWidth = paddedWidth;
|
|
|
final double targetHeight = targetWidth * device.aspectRatio;
|
|
final double targetHeight = targetWidth * device.aspectRatio;
|
|
|
|
|
|
|
|
- final double canvasWidth;
|
|
|
|
|
- final double canvasHeight;
|
|
|
|
|
-
|
|
|
|
|
if (targetHeight > paddedHeight) {
|
|
if (targetHeight > paddedHeight) {
|
|
|
- canvasHeight = paddedHeight;
|
|
|
|
|
- canvasWidth = paddedHeight / device.aspectRatio;
|
|
|
|
|
|
|
+ _cachedCanvasHeight = paddedHeight;
|
|
|
|
|
+ _cachedCanvasWidth = paddedHeight / device.aspectRatio;
|
|
|
} else {
|
|
} else {
|
|
|
- canvasWidth = targetWidth;
|
|
|
|
|
- canvasHeight = targetHeight;
|
|
|
|
|
|
|
+ _cachedCanvasWidth = targetWidth;
|
|
|
|
|
+ _cachedCanvasHeight = targetHeight;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ _layoutCalculated = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @override
|
|
|
|
|
+ Widget build(BuildContext context) {
|
|
|
|
|
+ if (isLoading) return scrollableDummy;
|
|
|
|
|
+
|
|
|
|
|
+ _calculateLayout();
|
|
|
|
|
+
|
|
|
return Scaffold(
|
|
return Scaffold(
|
|
|
backgroundColor: SkinHelper.colorWhite,
|
|
backgroundColor: SkinHelper.colorWhite,
|
|
|
appBar: AppBar(
|
|
appBar: AppBar(
|
|
|
backgroundColor: SkinHelper.colorWhite,
|
|
backgroundColor: SkinHelper.colorWhite,
|
|
|
- // elevation: 1,
|
|
|
|
|
centerTitle: true,
|
|
centerTitle: true,
|
|
|
leading: RepaintBoundary(
|
|
leading: RepaintBoundary(
|
|
|
- // !!! 改造点 3: 添加 ScaleTransition
|
|
|
|
|
- key: _collectionKey, // 关联 GlobalKey
|
|
|
|
|
|
|
+ key: _collectionKey,
|
|
|
child: ScaleTransition(
|
|
child: ScaleTransition(
|
|
|
- scale: _collectionAnimation, // 使用定义的放大/缩小动画
|
|
|
|
|
|
|
+ scale: _collectionAnimation,
|
|
|
child: IconButton(
|
|
child: IconButton(
|
|
|
onPressed: () {
|
|
onPressed: () {
|
|
|
audio.playSfx(SfxType.click);
|
|
audio.playSfx(SfxType.click);
|
|
@@ -362,27 +374,20 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
|
|
|
),
|
|
),
|
|
|
),
|
|
),
|
|
|
),
|
|
),
|
|
|
- // title: const Text(
|
|
|
|
|
- // 'Jigsort Solitaire',
|
|
|
|
|
- // style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 24),
|
|
|
|
|
- // ),
|
|
|
|
|
- // 🚀 改造点:将 Text 标题替换为 SvgPicture
|
|
|
|
|
title: SvgPicture.asset(
|
|
title: SvgPicture.asset(
|
|
|
- 'assets/images/title.svg', // 替换为您的 SVG 文件路径
|
|
|
|
|
- height: 32, // 根据您的设计调整高度,确保它在 AppBar 中显示良好
|
|
|
|
|
- // colorFilter: const ColorFilter.mode(Colors.black87, BlendMode.srcIn), // 如果SVG是单色,可以设置颜色
|
|
|
|
|
|
|
+ 'assets/images/title.svg',
|
|
|
|
|
+ height: 32,
|
|
|
placeholderBuilder: (BuildContext context) => const Text(
|
|
placeholderBuilder: (BuildContext context) => const Text(
|
|
|
- // 占位符,以防SVG加载失败
|
|
|
|
|
'Jigsort Solitaire',
|
|
'Jigsort Solitaire',
|
|
|
style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 24),
|
|
style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 24),
|
|
|
),
|
|
),
|
|
|
),
|
|
),
|
|
|
|
|
+ // title: // Release模式下显示内存信息
|
|
|
|
|
+ // MemoryMonitor.getMemoryWidget(),
|
|
|
actions: [
|
|
actions: [
|
|
|
IconButton(
|
|
IconButton(
|
|
|
onPressed: () {
|
|
onPressed: () {
|
|
|
audio.playSfx(SfxType.click);
|
|
audio.playSfx(SfxType.click);
|
|
|
- // AppLovinMAX.showMediationDebugger();
|
|
|
|
|
- // Navigator.push(context, SettingsDialog.buildRoute());
|
|
|
|
|
Navigator.push(context, SettingScreen.buildRoute());
|
|
Navigator.push(context, SettingScreen.buildRoute());
|
|
|
},
|
|
},
|
|
|
icon: const Icon(Icons.settings, color: Colors.black87),
|
|
icon: const Icon(Icons.settings, color: Colors.black87),
|
|
@@ -396,22 +401,20 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
|
|
|
child: Column(
|
|
child: Column(
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
|
children: [
|
|
children: [
|
|
|
- // 2. 画布区域(固定尺寸)
|
|
|
|
|
Padding(
|
|
Padding(
|
|
|
- padding: const EdgeInsets.symmetric(horizontal: 30), // 左右30px
|
|
|
|
|
|
|
+ padding: const EdgeInsets.symmetric(horizontal: 30),
|
|
|
child: SizedBox(
|
|
child: SizedBox(
|
|
|
- width: canvasWidth,
|
|
|
|
|
- height: canvasHeight,
|
|
|
|
|
|
|
+ width: _cachedCanvasWidth!,
|
|
|
|
|
+ height: _cachedCanvasHeight!,
|
|
|
child: ValueListenableBuilder(
|
|
child: ValueListenableBuilder(
|
|
|
valueListenable: data.completedWorks,
|
|
valueListenable: data.completedWorks,
|
|
|
builder: (context, value, child) {
|
|
builder: (context, value, child) {
|
|
|
return HomeBoardPlay(
|
|
return HomeBoardPlay(
|
|
|
key: _canvasKey,
|
|
key: _canvasKey,
|
|
|
- canvasWidth: canvasWidth,
|
|
|
|
|
- canvasHeight: canvasHeight,
|
|
|
|
|
|
|
+ canvasWidth: _cachedCanvasWidth!,
|
|
|
|
|
+ canvasHeight: _cachedCanvasHeight!,
|
|
|
collectionKey: _collectionKey,
|
|
collectionKey: _collectionKey,
|
|
|
onCollectionDone: () {
|
|
onCollectionDone: () {
|
|
|
- // collection unlocking 动画结束,启动collection button 的接收反馈动画
|
|
|
|
|
_log.info('onCollectionDone, 启动合集收纳反馈动画');
|
|
_log.info('onCollectionDone, 启动合集收纳反馈动画');
|
|
|
audio.playSfx(SfxType.appear);
|
|
audio.playSfx(SfxType.appear);
|
|
|
_collectionController.forward(from: 0.0);
|
|
_collectionController.forward(from: 0.0);
|
|
@@ -427,10 +430,9 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
|
|
|
),
|
|
),
|
|
|
SafeArea(
|
|
SafeArea(
|
|
|
child: SizedBox(
|
|
child: SizedBox(
|
|
|
- // 始终预留高度,防止 Banner 出现时下方 UI 整体上跳(Layout Jitter)
|
|
|
|
|
height: context.read<Device>().bannerHeight,
|
|
height: context.read<Device>().bannerHeight,
|
|
|
width: double.infinity,
|
|
width: double.infinity,
|
|
|
- child: isBannerVisible ? getBanner('home_bottom') : const SizedBox.shrink(), // 隐藏时完全不占位或保持留白
|
|
|
|
|
|
|
+ child: isBannerVisible ? getBanner('home_bottom') : const SizedBox.shrink(),
|
|
|
),
|
|
),
|
|
|
),
|
|
),
|
|
|
],
|
|
],
|
|
@@ -438,42 +440,61 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ✅ 优化点10: 导航状态管理
|
|
|
void gotoPlay(ListItem item, {bool firstRun = false}) async {
|
|
void gotoPlay(ListItem item, {bool firstRun = false}) async {
|
|
|
|
|
+ if (_isNavigating) {
|
|
|
|
|
+ _log.info('Navigation already in progress, ignoring');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
_log.info('goto play, firstRun = $firstRun');
|
|
_log.info('goto play, firstRun = $firstRun');
|
|
|
|
|
|
|
|
- // !!! 增加保护
|
|
|
|
|
if (!mounted) return;
|
|
if (!mounted) return;
|
|
|
|
|
|
|
|
- PageRouteBuilder? pageRouteBuilder = BoardPlay.buildRoute(item, firstRun: firstRun);
|
|
|
|
|
- final result = await Navigator.push(context, pageRouteBuilder);
|
|
|
|
|
|
|
+ _isNavigating = true;
|
|
|
|
|
|
|
|
- if (!mounted) return;
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ cleanBanner();
|
|
|
|
|
|
|
|
- if (result != null && result == true) {
|
|
|
|
|
- // 通关返回, 展示翻牌
|
|
|
|
|
- _canvasKey.currentState?.startFlipAnimation();
|
|
|
|
|
|
|
+ if (!mounted) return;
|
|
|
|
|
+
|
|
|
|
|
+ PageRouteBuilder? pageRouteBuilder = BoardPlay.buildRoute(item, firstRun: firstRun);
|
|
|
|
|
+ final result = await Navigator.push(context, pageRouteBuilder);
|
|
|
|
|
+
|
|
|
|
|
+ if (!mounted) return;
|
|
|
|
|
+
|
|
|
|
|
+ // result 是 true, 说明关卡已经完成。 但此时可能播放插屏广告中, 需要等待用户关闭广告之后才能执行翻牌等逻辑
|
|
|
|
|
+ if (result != null && result == true) {
|
|
|
|
|
+ // 打印下当下的插屏广告状态
|
|
|
|
|
+ _log.info('==================>interState = $intersState');
|
|
|
|
|
+
|
|
|
|
|
+ if (intersState == AdState.ready) {
|
|
|
|
|
+ // 这种情况表示有插屏广告在播放,需要等待广告关闭之后才能执行翻牌动画等逻辑
|
|
|
|
|
+ interPending = true;
|
|
|
|
|
+ _log.info('Interstitial ad is currently playing, will execute post-ad logic after dismissal.');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- final bool hasSufficientData = latest != null && latest!.length >= minimumRemoteLoadCount;
|
|
|
|
|
- final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch;
|
|
|
|
|
|
|
+ _canvasKey.currentState?.startFlipAnimation();
|
|
|
|
|
|
|
|
- if (hasSufficientData) {
|
|
|
|
|
- // 1. 数据完整:如果网络活跃,立即顺延预加载。
|
|
|
|
|
- if (isNetworkActive) {
|
|
|
|
|
- _log.info('Game finished, data complete & Network Active. Triggering sequential preloading...');
|
|
|
|
|
- _preloadNextImages();
|
|
|
|
|
|
|
+ final bool hasSufficientData = latest != null && latest!.length >= minimumRemoteLoadCount;
|
|
|
|
|
+ final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch;
|
|
|
|
|
+
|
|
|
|
|
+ if (hasSufficientData) {
|
|
|
|
|
+ if (isNetworkActive) {
|
|
|
|
|
+ _log.info('Game finished, data complete & Network Active. Triggering sequential preloading...');
|
|
|
|
|
+ _preloadNextImages();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ _log.info('Game finished, data complete but Network inactive. Attempting refresh.');
|
|
|
|
|
+ refresh();
|
|
|
|
|
+ }
|
|
|
} else {
|
|
} else {
|
|
|
- // 2. 数据完整但网络不活跃/状态未知:尝试刷新,让 _onLatestDataUpdate 负责后续处理
|
|
|
|
|
- _log.info('Game finished, data complete but Network inactive. Attempting refresh.');
|
|
|
|
|
|
|
+ _log.info('Game finished, remote data incomplete. Attempting refresh...');
|
|
|
refresh();
|
|
refresh();
|
|
|
}
|
|
}
|
|
|
- } else {
|
|
|
|
|
- // 3. 数据不完整:无论如何都需要刷新,让 _onLatestDataUpdate 重新处理
|
|
|
|
|
- _log.info('Game finished, remote data incomplete. Attempting refresh...');
|
|
|
|
|
- refresh();
|
|
|
|
|
}
|
|
}
|
|
|
- } else {
|
|
|
|
|
- // 非关卡通关返回,在这里播放插屏广告
|
|
|
|
|
- // showInterstitialAd("level_exit", currentItem!.id, data.currentLevel);
|
|
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ _isNavigating = false;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -504,43 +525,22 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
|
|
|
children: [
|
|
children: [
|
|
|
Text(
|
|
Text(
|
|
|
AppLocalizations.of(context)!.play,
|
|
AppLocalizations.of(context)!.play,
|
|
|
- style: TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold),
|
|
|
|
|
- ),
|
|
|
|
|
- ValueListenableBuilder<List<Work>>(
|
|
|
|
|
- valueListenable: data.completedWorks,
|
|
|
|
|
- builder: (context, isSoundOn, child) {
|
|
|
|
|
- return Text('${AppLocalizations.of(context)!.level} ${data.currentLevel + 1}', style: const TextStyle(color: Colors.white, fontSize: 16));
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold),
|
|
|
),
|
|
),
|
|
|
|
|
+ Text('${AppLocalizations.of(context)!.level} ${data.currentLevel + 1}', style: const TextStyle(color: Colors.white, fontSize: 16)),
|
|
|
],
|
|
],
|
|
|
),
|
|
),
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Widget get scrollableDummy => Scaffold(
|
|
|
|
|
- // body: 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)])),
|
|
|
|
|
- // ),
|
|
|
|
|
- // );
|
|
|
|
|
- // },
|
|
|
|
|
- // ),
|
|
|
|
|
- // );
|
|
|
|
|
-
|
|
|
|
|
Widget get scrollableDummy => Scaffold(
|
|
Widget get scrollableDummy => Scaffold(
|
|
|
backgroundColor: SkinHelper.colorWhite,
|
|
backgroundColor: SkinHelper.colorWhite,
|
|
|
body: Center(
|
|
body: Center(
|
|
|
child: Column(
|
|
child: Column(
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
children: [
|
|
children: [
|
|
|
- // 使用原生最轻量的进度指示器
|
|
|
|
|
SizedBox(width: 40, height: 40, child: CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation<Color>(SkinHelper.coreBgColor))),
|
|
SizedBox(width: 40, height: 40, child: CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation<Color>(SkinHelper.coreBgColor))),
|
|
|
const SizedBox(height: 20),
|
|
const SizedBox(height: 20),
|
|
|
- // 可选:添加一个简单的文字,让用户知道在加载
|
|
|
|
|
Text(
|
|
Text(
|
|
|
"Loading...",
|
|
"Loading...",
|
|
|
style: TextStyle(color: SkinHelper.slotBorderColor.withOpacity(0.7), fontSize: 14, fontWeight: FontWeight.w500),
|
|
style: TextStyle(color: SkinHelper.slotBorderColor.withOpacity(0.7), fontSize: 14, fontWeight: FontWeight.w500),
|
|
@@ -553,82 +553,39 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
|
|
|
///////////////////////// 初始化相关 /////////////////////////
|
|
///////////////////////// 初始化相关 /////////////////////////
|
|
|
|
|
|
|
|
static bool hasInit = false;
|
|
static bool hasInit = false;
|
|
|
- static MyMethodChannel platform = MyMethodChannel();
|
|
|
|
|
|
|
|
|
|
- // 在列表刷出来后才正式初始化admod等组件
|
|
|
|
|
void initThird() async {
|
|
void initThird() async {
|
|
|
if (hasInit) return;
|
|
if (hasInit) return;
|
|
|
-
|
|
|
|
|
hasInit = true;
|
|
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;
|
|
TrackingStatus attStatus = await AppTrackingTransparency.trackingAuthorizationStatus;
|
|
|
if (attStatus == TrackingStatus.authorized && Platform.isIOS) {
|
|
if (attStatus == TrackingStatus.authorized && Platform.isIOS) {
|
|
|
// ATT 通过之后,ios需要调用相关的原生sdk接口做进一步的初始化
|
|
// ATT 通过之后,ios需要调用相关的原生sdk接口做进一步的初始化
|
|
|
- // await platform.setHasUserConsent(true);
|
|
|
|
|
- // await platform.setAdvertiserTrackingEnabled(true);
|
|
|
|
|
}
|
|
}
|
|
|
- initFCM(); // 消息推送许可弹窗
|
|
|
|
|
- initAd(); // admod 的广告加载安排在iOS ATT 之后,以便能够加载到个性化广告
|
|
|
|
|
- AdjustHelper.init(Persistence().uuid); // 初始化Adjust
|
|
|
|
|
|
|
+ initFCM();
|
|
|
|
|
+ initAd();
|
|
|
|
|
+ AdjustHelper.init(Persistence().uuid);
|
|
|
|
|
|
|
|
final idfa = await AppTrackingTransparency.getAdvertisingIdentifier();
|
|
final idfa = await AppTrackingTransparency.getAdvertisingIdentifier();
|
|
|
_log.info("idfa: $idfa");
|
|
_log.info("idfa: $idfa");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /////////////////////////// ATT ///////////////////////////
|
|
|
|
|
- // Platform messages are asynchronous, so we initialize in an async method.
|
|
|
|
|
Future<bool> initATT() async {
|
|
Future<bool> initATT() async {
|
|
|
TrackingStatus status = await AppTrackingTransparency.trackingAuthorizationStatus;
|
|
TrackingStatus status = await AppTrackingTransparency.trackingAuthorizationStatus;
|
|
|
_log.info('initATT111 $status');
|
|
_log.info('initATT111 $status');
|
|
|
- // If the system can show an authorization request dialog
|
|
|
|
|
if (status == TrackingStatus.notDetermined) {
|
|
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();
|
|
status = await AppTrackingTransparency.requestTrackingAuthorization();
|
|
|
_log.info('initATT222 $status');
|
|
_log.info('initATT222 $status');
|
|
|
}
|
|
}
|
|
|
- if (status == TrackingStatus.authorized) {
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
- return false;
|
|
|
|
|
|
|
+ return status == TrackingStatus.authorized;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 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() {
|
|
initAd() {
|
|
|
_log.info('initAd');
|
|
_log.info('initAd');
|
|
|
- // AdsController adsController = context.read<AdsController>();
|
|
|
|
|
- // adsController.initialize();
|
|
|
|
|
ApplovinAdsController applovinAdsController = context.read<ApplovinAdsController>();
|
|
ApplovinAdsController applovinAdsController = context.read<ApplovinAdsController>();
|
|
|
applovinAdsController.initialize();
|
|
applovinAdsController.initialize();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /////////////////////////// FCM ///////////////////////////
|
|
|
|
|
- // 消息推送许可弹框
|
|
|
|
|
initFCM() async {
|
|
initFCM() async {
|
|
|
try {
|
|
try {
|
|
|
final fcmToken = await FirebaseMessaging.instance.getToken();
|
|
final fcmToken = await FirebaseMessaging.instance.getToken();
|