| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618 |
- import 'dart:async';
- import 'dart:io';
- import 'dart:math';
- 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:flutter_svg/svg.dart';
- import 'package:fluttertoast/fluttertoast.dart';
- import 'package:logging/logging.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';
- import 'package:puzzleweave/config/config.dart';
- import 'package:puzzleweave/config/device.dart';
- import 'package:puzzleweave/firebase/adjust_helper.dart';
- 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/download.dart';
- import 'package:puzzleweave/models/items.dart';
- import 'package:puzzleweave/persistence/persistence.dart';
- import 'package:puzzleweave/play/board_play.dart';
- import 'package:puzzleweave/settings/settings_screen.dart';
- import 'package:puzzleweave/skin/skin.dart';
- import 'package:puzzleweave/utils/mybutton.dart';
- import 'package:puzzleweave/utils/utils.dart';
- import '../ads/ad_helper.dart';
- import '../ads/ads_state.dart';
- import '../utils/memory_monitor.dart';
- final Logger _log = Logger('home_screen');
- class HomeScreen extends StatefulWidget {
- const HomeScreen({super.key});
- @override
- State<StatefulWidget> createState() => _HomeScreen();
- }
- const int minimumRemoteLoadCount = 30;
- class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
- late Device device;
- late JcAudioController audio;
- List<ListItem>? latest;
- late CachedRequest latestCachedRequest;
- late StreamSubscription? latestSubscription;
- final _canvasKey = GlobalKey<HomeBoardPlayState>();
- final GlobalKey _collectionKey = GlobalKey();
- bool isLoading = true;
- bool firstRun = false;
- // ✅ 优化点1: 导航状态管理
- bool _isNavigating = false;
- // ✅ 优化点2: 防抖机制
- Timer? _refreshDebouncer;
- // ✅ 优化点3: 缓存计算结果
- double? _cachedCanvasWidth;
- double? _cachedCanvasHeight;
- bool _layoutCalculated = false;
- late AnimationController _collectionController;
- late Animation<double> _collectionAnimation;
- bool interPending = false;
- @override
- void initState() {
- super.initState();
- _log.info("首页初始化");
- _initializeComponents();
- _setupAnimations();
- _handleInitialNavigation();
- }
- void _initializeComponents() {
- device = context.read<Device>();
- audio = context.read<JcAudioController>();
- latestCachedRequest = data.latest;
- final cachedData = latestCachedRequest.cachedData;
- if (cachedData != null) {
- _onLatestDataUpdate(cachedData);
- }
- latestSubscription = latestCachedRequest.stream.listen(_onLatestDataUpdate, onError: _onLatestDataError);
- 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);
- }
- });
- _collectionAnimation = TweenSequence<double>([
- TweenSequenceItem(tween: Tween<double>(begin: 1.0, end: 1.4).chain(CurveTween(curve: Curves.easeOut)), weight: 40.0),
- TweenSequenceItem(tween: Tween<double>(begin: 1.4, end: 1.0).chain(CurveTween(curve: Curves.easeIn)), weight: 60.0),
- ]).animate(_collectionController);
- }
- void _handleInitialNavigation() {
- if (Persistence().firstRun) {
- firstRun = true;
- WidgetsBinding.instance.addPostFrameCallback((_) {
- if (mounted && !_isNavigating) {
- _handleFirstRunNavigation();
- }
- });
- Persistence().firstRun = false;
- }
- WidgetsBinding.instance.addPostFrameCallback((_) {
- if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed && mounted) {
- audio.startMusic();
- }
- });
- }
- // ✅ 优化点4: 异步化首次运行导航
- void _handleFirstRunNavigation() {
- if (_isNavigating) return;
- _log.info('First run detected, navigating to initial play page.');
- final AssetItem initialItem = AssetItem(
- Config.firstId,
- '',
- 2000,
- 3000,
- 3,
- false,
- 'assets/builtin/${Config.firstId}.jpeg',
- 'assets/builtin/${Config.firstId}.jpeg',
- );
- return gotoPlay(initialItem, firstRun: true);
- }
- // ✅ 优化点5: 异步化文件检查
- void checkGoPlay() {
- if (currentItem == null || _isNavigating) return;
- Future.microtask(() async {
- try {
- final jsonFile = await localFile(currentItem!.jsonPath);
- final exists = await jsonFile.exists();
- if (mounted && exists && !_isNavigating) {
- gotoPlay(currentItem!);
- }
- } catch (e) {
- _log.warning('Error checking play file: $e');
- }
- });
- }
- @override
- void dispose() {
- // 清理 banner 广告资源
- cleanBanner();
- _refreshDebouncer?.cancel();
- latestSubscription?.cancel();
- _collectionController.dispose();
- super.dispose();
- }
- // ✅ 优化点6: 简化数据更新逻辑
- _onLatestDataUpdate(datalist) {
- _log.info('_onLatestDataUpdate....');
- if (datalist == null) return;
- bool shouldCheckGoPlay = false;
- if (currentItem == null && !firstRun) {
- shouldCheckGoPlay = true;
- }
- latest = datalist as List<ListItem>;
- isLoading = false;
- setState(() {});
- final bool hasSufficientData = datalist.length >= minimumRemoteLoadCount;
- final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch;
- if (hasSufficientData) {
- if (!hasInit) {
- initThird();
- }
- if (!isNetworkActive) {
- _debouncedRefresh();
- }
- } else {
- _log.info('Data insufficient (only ${datalist.length} items). Attempting refresh.');
- _debouncedRefresh();
- }
- if (shouldCheckGoPlay) {
- checkGoPlay();
- }
- }
- _onLatestDataError(error) {
- _log.info('_onLatestDataError.... $error');
- if (latest == null || latest!.isEmpty || latest!.length < 20) {
- _log.warning("_onLatestDataError, retry again");
- _debouncedRefresh();
- }
- }
- // ✅ 优化点7: 防抖刷新
- void _debouncedRefresh() {
- _refreshDebouncer?.cancel();
- _refreshDebouncer = Timer(const Duration(seconds: 2), () {
- if (mounted) {
- refresh();
- }
- });
- }
- Future<void> refresh() async {
- _log.info('refresh...');
- await latestCachedRequest.refresh();
- }
- ListItem? get currentItem {
- if (latest == null || latest!.isEmpty) {
- return null;
- }
- final Set<String> completedIds = data.completedWorks.value.map((work) => work.id).toSet();
- for (final item in latest!) {
- final String itemId = item.id;
- if (!completedIds.contains(itemId)) {
- _log.info('Found current item: $itemId');
- return item;
- }
- }
- _log.info('All items in the latest list have been completed.');
- return null;
- }
- // ✅ 优化点8: 后台预加载
- void _preloadNextImages() {
- Future.microtask(() async {
- const int totalPreloadCount = 20;
- if (latest == null || latest!.isEmpty || latest!.length < minimumRemoteLoadCount) {
- _log.info('Preload failed: latest list is empty.');
- return;
- }
- 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;
- }
- final int endPreloadIndex = min(startIndex + totalPreloadCount, latest!.length);
- final List<ListItem> itemsToLoad = latest!.sublist(startIndex, endPreloadIndex);
- if (itemsToLoad.isEmpty) {
- _log.info('Preload: No items found in the range.');
- return;
- }
- final ListItem currentItemToLoad = itemsToLoad.removeAt(0);
- itemsToLoad.add(currentItemToLoad);
- _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.');
- });
- }
- // ✅ 优化点9: 缓存布局计算
- void _calculateLayout() {
- if (_layoutCalculated) return;
- final double availableHeight = device.screenSize.height - device.appBarHeight - device.bannerHeight - 120;
- final double paddedWidth = device.screenSize.width - 2 * 30;
- final double paddedHeight = availableHeight;
- final double targetWidth = paddedWidth;
- final double targetHeight = targetWidth * device.aspectRatio;
- if (targetHeight > paddedHeight) {
- _cachedCanvasHeight = paddedHeight;
- _cachedCanvasWidth = paddedHeight / device.aspectRatio;
- } else {
- _cachedCanvasWidth = targetWidth;
- _cachedCanvasHeight = targetHeight;
- }
- _layoutCalculated = true;
- }
- @override
- Widget build(BuildContext context) {
- if (isLoading) return scrollableDummy;
- _calculateLayout();
- return Scaffold(
- backgroundColor: SkinHelper.colorWhite,
- appBar: AppBar(
- backgroundColor: SkinHelper.colorWhite,
- centerTitle: true,
- leading: RepaintBoundary(
- key: _collectionKey,
- child: ScaleTransition(
- scale: _collectionAnimation,
- child: IconButton(
- onPressed: () {
- audio.playSfx(SfxType.click);
- cleanBanner();
- Navigator.push(context, CollectionScreen.buildRoute());
- },
- icon: const Icon(Icons.collections, color: Colors.black87),
- ),
- ),
- ),
- title: SvgPicture.asset(
- 'assets/images/title.svg',
- height: 32,
- placeholderBuilder: (BuildContext context) => const Text(
- 'Jigsort Solitaire',
- style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 24),
- ),
- ),
- actions: [
- IconButton(
- onPressed: () {
- audio.playSfx(SfxType.click);
- cleanBanner();
- Navigator.push(context, SettingScreen.buildRoute());
- // showAppLovinDebugger();
- },
- icon: const Icon(Icons.settings, color: Colors.black87),
- ),
- ],
- ),
- body: Column(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- if (Config.isDebug) MemoryMonitor.getMemoryWidget(),
- Expanded(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 30),
- child: SizedBox(
- width: _cachedCanvasWidth!,
- height: _cachedCanvasHeight!,
- child: ValueListenableBuilder(
- valueListenable: data.completedWorks,
- builder: (context, value, child) {
- return HomeBoardPlay(
- key: _canvasKey,
- canvasWidth: _cachedCanvasWidth!,
- canvasHeight: _cachedCanvasHeight!,
- collectionKey: _collectionKey,
- onCollectionDone: () {
- _log.info('onCollectionDone, 启动合集收纳反馈动画');
- audio.playSfx(SfxType.appear);
- _collectionController.forward(from: 0.0);
- },
- );
- },
- ),
- ),
- ),
- playButton,
- ],
- ),
- ),
- SafeArea(
- child: SizedBox(
- height: context.read<Device>().bannerHeight,
- width: double.infinity,
- child: isBannerVisible ? getBanner('home_bottom') : const SizedBox.shrink(),
- ),
- ),
- ],
- ),
- );
- }
- // ✅ 优化点10: 导航状态管理
- void gotoPlay(ListItem item, {bool firstRun = false}) async {
- if (_isNavigating) {
- _log.info('Navigation already in progress, ignoring');
- return;
- }
- _log.info('goto play, firstRun = $firstRun');
- if (!mounted) return;
- _isNavigating = true;
- interPending = false;
- try {
- cleanBanner();
- 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 && data.currentLevel % 25 != 0 && shouldShowInterstitialAd("level_done", data.currentLevel - 1)) {
- // 这种情况表示有插屏广告在播放,需要等待广告关闭之后才能执行翻牌动画等逻辑
- interPending = true;
- _log.info('Interstitial ad is currently playing, will execute post-ad logic after dismissal.');
- return;
- }
- _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();
- }
- }
- } finally {
- _isNavigating = false;
- }
- }
- Widget get playButton {
- return MyElevatedButton(
- width: device.isTablet ? 300 : 200,
- height: 70,
- borderRadius: BorderRadius.circular(20),
- gradient: LinearGradient(colors: [SkinHelper.coreBgColor, SkinHelper.slotBorderColor]),
- onPressed: () async {
- audio.playSfx(SfxType.click);
- if (currentItem != null) {
- gotoPlay(currentItem!);
- } else {
- Fluttertoast.showToast(
- msg: AppLocalizations.of(context)!.noMorePicture,
- toastLength: Toast.LENGTH_SHORT,
- gravity: ToastGravity.CENTER,
- timeInSecForIosWeb: 1,
- backgroundColor: SkinHelper.slotBorderColor,
- textColor: Colors.white,
- fontSize: 16.0,
- );
- }
- },
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Text(
- AppLocalizations.of(context)!.play,
- 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(
- backgroundColor: SkinHelper.colorWhite,
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- SizedBox(width: 40, height: 40, child: CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation<Color>(SkinHelper.coreBgColor))),
- const SizedBox(height: 20),
- Text(
- "Loading...",
- style: TextStyle(color: SkinHelper.slotBorderColor.withOpacity(0.7), fontSize: 14, fontWeight: FontWeight.w500),
- ),
- ],
- ),
- ),
- );
- ///////////////////////// 初始化相关 /////////////////////////
- static bool hasInit = false;
- void initThird() async {
- if (hasInit) return;
- hasInit = true;
- // TrackingStatus attStatus = await AppTrackingTransparency.trackingAuthorizationStatus;
- // if (attStatus == TrackingStatus.authorized && Platform.isIOS) {
- // // ATT 通过之后,ios需要调用相关的原生sdk接口做进一步的初始化
- // }
- initFCM();
- initAd();
- AdjustHelper.init(Persistence().uuid);
- // final idfa = await AppTrackingTransparency.getAdvertisingIdentifier();
- // _log.info("idfa: $idfa");
- }
- Future<bool> initATT() async {
- TrackingStatus status = await AppTrackingTransparency.trackingAuthorizationStatus;
- _log.info('initATT111 $status');
- if (status == TrackingStatus.notDetermined) {
- status = await AppTrackingTransparency.requestTrackingAuthorization();
- _log.info('initATT222 $status');
- }
- return status == TrackingStatus.authorized;
- }
- initAd() {
- _log.info('initAd');
- ApplovinAdsController applovinAdsController = context.read<ApplovinAdsController>();
- applovinAdsController.initialize();
- }
- 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);
- }
- }
- }
|