|
|
@@ -1,5 +1,6 @@
|
|
|
import 'dart:async';
|
|
|
import 'dart:io';
|
|
|
+import 'dart:math';
|
|
|
|
|
|
import 'package:advertising_id/advertising_id.dart';
|
|
|
import 'package:app_tracking_transparency/app_tracking_transparency.dart';
|
|
|
@@ -8,7 +9,11 @@ import 'package:firebase_messaging/firebase_messaging.dart';
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
import 'package:flutter/material.dart';
|
|
|
import 'package:flutter/services.dart';
|
|
|
+import 'package:flutter_svg/svg.dart';
|
|
|
import 'package:fluttertoast/fluttertoast.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/collection/collection_screen.dart';
|
|
|
@@ -17,17 +22,13 @@ import 'package:puzzleweave/homepage/home_board_play.dart';
|
|
|
import 'package:puzzleweave/l10n/app_localizations.dart';
|
|
|
import 'package:puzzleweave/models/cached_request.dart';
|
|
|
import 'package:puzzleweave/models/data.dart';
|
|
|
+import 'package:puzzleweave/models/download.dart';
|
|
|
import 'package:puzzleweave/models/items.dart';
|
|
|
import 'package:puzzleweave/platform/my_method_channel.dart';
|
|
|
import 'package:puzzleweave/play/board_play.dart';
|
|
|
-import 'package:puzzleweave/settings/settings_dialog.dart';
|
|
|
import 'package:puzzleweave/settings/settings_screen.dart';
|
|
|
import 'package:puzzleweave/skin/skin.dart';
|
|
|
import 'package:puzzleweave/utils/mybutton.dart';
|
|
|
-import 'package:logging/logging.dart';
|
|
|
-import 'package:lottie/lottie.dart';
|
|
|
-import 'package:provider/provider.dart';
|
|
|
-import '../ads/ads_state.dart';
|
|
|
|
|
|
final Logger _log = Logger('home_screen');
|
|
|
|
|
|
@@ -38,6 +39,8 @@ class HomeScreen extends StatefulWidget {
|
|
|
State<StatefulWidget> createState() => _HomeScreen();
|
|
|
}
|
|
|
|
|
|
+const int minimumRemoteLoadCount = 30; // 假设加载到 30 张图才算网络畅通
|
|
|
+
|
|
|
class _HomeScreen extends State<HomeScreen> with TickerProviderStateMixin {
|
|
|
late Device device;
|
|
|
late JcAudioController audio;
|
|
|
@@ -80,7 +83,7 @@ class _HomeScreen extends State<HomeScreen> with TickerProviderStateMixin {
|
|
|
vsync: this,
|
|
|
)..addStatusListener((status) {
|
|
|
if (status == AnimationStatus.completed) {
|
|
|
- _canvasKey.currentState?.switchToNextCollection();
|
|
|
+ audio.playSfx(SfxType.pop);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
@@ -108,10 +111,31 @@ class _HomeScreen extends State<HomeScreen> with TickerProviderStateMixin {
|
|
|
latest = data as List<ListItem>;
|
|
|
isLoading = false;
|
|
|
setState(() {});
|
|
|
- if (data.length >= 20) {
|
|
|
- // 远程latest列表已加载,说明网络已通,这个时候再来初始化Admod,ATT, UMP这些东西
|
|
|
- initThird();
|
|
|
+
|
|
|
+ // 1. 检查数据量是否达到最低要求 (>= 30)
|
|
|
+ final bool hasSufficientData = data.length >= minimumRemoteLoadCount;
|
|
|
+
|
|
|
+ // 2. 检查数据是否来自最近一次成功的网络请求
|
|
|
+ final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch; // !!! 关键检查点
|
|
|
+
|
|
|
+ if (hasSufficientData) {
|
|
|
+ // 如果数据完整,无论是否是缓存数据,都尝试初始化第三方服务(因为主页已经可以显示了)
|
|
|
+ if (!hasInit) {
|
|
|
+ initThird();
|
|
|
+ }
|
|
|
+
|
|
|
+ // !!! 核心修改:只有在数据完整且最近网络请求成功时,才启动预加载
|
|
|
+ if (isNetworkActive) {
|
|
|
+ _log.info('Data sufficient AND Network Active. Starting preload.');
|
|
|
+ _preloadNextImages();
|
|
|
+ } else {
|
|
|
+ // 数据完整,但来自缓存,网络状态未知,3秒后尝试刷新(refresh)
|
|
|
+ _log.info('Data sufficient BUT Network status unknown/inactive. Attempting refresh in 3s.');
|
|
|
+ Future.delayed(Duration(seconds: 3), () => refresh());
|
|
|
+ }
|
|
|
} else {
|
|
|
+ // 数据不足 (例如,只有内置图),无论是缓存还是远程失败,都需要重试
|
|
|
+ _log.info('Data insufficient (only ${data.length} items). Attempting refresh in 3s.');
|
|
|
Future.delayed(Duration(seconds: 3), () => refresh());
|
|
|
}
|
|
|
}
|
|
|
@@ -170,6 +194,70 @@ class _HomeScreen extends State<HomeScreen> with TickerProviderStateMixin {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
+ /// 预加载未来 N 张图片到磁盘,并最后触发当前关卡下载以最大化内存缓存命中率。
|
|
|
+ void _preloadNextImages() {
|
|
|
+ // 预加载数量 (包括当前关卡在内,共 20 个)
|
|
|
+ const int totalPreloadCount = 20;
|
|
|
+
|
|
|
+ // 1. 确保 latest 数据已加载
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (startIndex == -1) {
|
|
|
+ _log.info('Preload: All images completed, nothing to preload.');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确定预加载范围 (从当前图片startIndex到 totalPreloadCount 个图片)
|
|
|
+ final int endPreloadIndex = min(startIndex + totalPreloadCount, latest!.length);
|
|
|
+
|
|
|
+ // 3. 准备要加载的列表 (从 startIndex 开始)
|
|
|
+ final List<ListItem> itemsToLoad = latest!.sublist(startIndex, endPreloadIndex);
|
|
|
+
|
|
|
+ if (itemsToLoad.isEmpty) {
|
|
|
+ _log.info('Preload: No items found in the range.');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 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 itemToLoad in itemsToLoad) {
|
|
|
+ // 对远程图片进行预加载
|
|
|
+ // 调用 ItemLoader.load,它会使用 Download 单例进行下载和缓存
|
|
|
+ // 我们不关心返回值或 Future,只是触发下载
|
|
|
+ if (itemToLoad is RemoteItem) {
|
|
|
+ try {
|
|
|
+ // 触发下载。对于非当前关卡,下载器会完成下载并写入磁盘,然后可能释放内存。
|
|
|
+ // 对于当前关卡 (最后一个被调用的),它留在内存中的可能性最大。
|
|
|
+ ItemLoader.load(itemToLoad);
|
|
|
+ preloadCount++;
|
|
|
+ } catch (e) {
|
|
|
+ _log.warning('Failed to load item for preloading: ${itemToLoad.id}, error: $e');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ _log.info('Preload initiated for $preloadCount remote images, current item was last.');
|
|
|
+ }
|
|
|
+
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
if (isLoading) return scrollableDummy;
|
|
|
@@ -216,9 +304,20 @@ class _HomeScreen extends State<HomeScreen> with TickerProviderStateMixin {
|
|
|
),
|
|
|
),
|
|
|
),
|
|
|
- title: const Text(
|
|
|
- 'Jigsort Solitaire',
|
|
|
- style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 24),
|
|
|
+ // title: const Text(
|
|
|
+ // 'Jigsort Solitaire',
|
|
|
+ // style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 24),
|
|
|
+ // ),
|
|
|
+ // 🚀 改造点:将 Text 标题替换为 SvgPicture
|
|
|
+ title: SvgPicture.asset(
|
|
|
+ 'assets/images/title.svg', // 替换为您的 SVG 文件路径
|
|
|
+ height: 32, // 根据您的设计调整高度,确保它在 AppBar 中显示良好
|
|
|
+ // colorFilter: const ColorFilter.mode(Colors.black87, BlendMode.srcIn), // 如果SVG是单色,可以设置颜色
|
|
|
+ placeholderBuilder: (BuildContext context) => const Text(
|
|
|
+ // 占位符,以防SVG加载失败
|
|
|
+ 'Jigsort Solitaire',
|
|
|
+ style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 24),
|
|
|
+ ),
|
|
|
),
|
|
|
actions: [
|
|
|
IconButton(
|
|
|
@@ -306,6 +405,24 @@ class _HomeScreen extends State<HomeScreen> with TickerProviderStateMixin {
|
|
|
final result = await Navigator.push(context, pageRouteBuilder);
|
|
|
if (result == true) {
|
|
|
_canvasKey.currentState?.startFlipAnimation();
|
|
|
+ final bool hasSufficientData = latest != null && latest!.length >= minimumRemoteLoadCount;
|
|
|
+ final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch;
|
|
|
+
|
|
|
+ if (hasSufficientData) {
|
|
|
+ // 1. 数据完整:如果网络活跃,立即顺延预加载。
|
|
|
+ if (isNetworkActive) {
|
|
|
+ _log.info('Game finished, data complete & Network Active. Triggering sequential preloading...');
|
|
|
+ _preloadNextImages();
|
|
|
+ } else {
|
|
|
+ // 2. 数据完整但网络不活跃/状态未知:尝试刷新,让 _onLatestDataUpdate 负责后续处理
|
|
|
+ _log.info('Game finished, data complete but Network inactive. Attempting refresh.');
|
|
|
+ refresh();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 3. 数据不完整:无论如何都需要刷新,让 _onLatestDataUpdate 重新处理
|
|
|
+ _log.info('Game finished, remote data incomplete. Attempting refresh...');
|
|
|
+ refresh();
|
|
|
+ }
|
|
|
}
|
|
|
} else {
|
|
|
Fluttertoast.showToast(
|