ソースを参照

add admod, meta, unity, moloco, mintegral adapter

guoziyun 5 ヶ月 前
コミット
3d44e016de

+ 26 - 0
android/app/build.gradle.kts

@@ -43,6 +43,9 @@ android {
         targetSdk = flutter.targetSdkVersion
         versionCode = flutter.versionCode
         versionName = flutter.versionName
+
+        // ✅ 建议加上这一行,确保 5 个广告 SDK 不会撑破方法数限制
+        multiDexEnabled = true
     }
 
     signingConfigs {
@@ -75,3 +78,26 @@ android {
 flutter {
     source = "../.."
 }
+
+dependencies {
+    // ✅ MultiDex 支持库
+    implementation("androidx.multidex:multidex:2.0.1")
+
+    // AppLovin MAX 核心适配器 (建议使用最新版本或与你的 AppLovin SDK 匹配的版本)
+    // 注意:Kotlin DSL 使用 implementation("...") 括号加双引号
+    
+    // 1. AdMob (Google) 适配器
+    implementation("com.applovin.mediation:google-adapter:23.3.0.0")
+    
+    // 2. Meta (Facebook) 适配器
+    implementation("com.applovin.mediation:facebook-adapter:6.17.0.0")
+
+    // 3. UnityAds
+    implementation("com.applovin.mediation:unityads-adapter:4.12.2.0")
+
+    // 4. Moloco
+    implementation("com.applovin.mediation:moloco-adapter:3.0.0.0")
+
+    // 5. Mintegral
+    implementation("com.applovin.mediation:mintegral-adapter:16.8.51.0")
+}

+ 60 - 0
android/app/proguard-rules.pro

@@ -0,0 +1,60 @@
+# ---------------------------------------------------------
+# 1. AppLovin MAX 核心规则
+# ---------------------------------------------------------
+-keep class com.applovin.** { *; }
+-dontwarn com.applovin.**
+
+# ---------------------------------------------------------
+# 2. Google AdMob 规则
+# ---------------------------------------------------------
+-keep class com.google.android.gms.ads.** { *; }
+-keep class com.google.android.gms.common.** { *; }
+-dontwarn com.google.android.gms.**
+
+# ---------------------------------------------------------
+# 3. Meta (Facebook) 规则
+# ---------------------------------------------------------
+-keep class com.facebook.ads.** { *; }
+-dontwarn com.facebook.ads.**
+
+# ---------------------------------------------------------
+# 4. Unity Ads 规则
+# ---------------------------------------------------------
+-keep class com.unity3d.ads.** { *; }
+-keep class com.unity3d.services.** { *; }
+-dontwarn com.unity3d.ads.**
+
+# ---------------------------------------------------------
+# 5. Moloco 规则
+# ---------------------------------------------------------
+-keep class com.moloco.** { *; }
+-dontwarn com.moloco.**
+
+# ---------------------------------------------------------
+# 6. Mintegral (MBridge) 规则
+# ---------------------------------------------------------
+-keep class com.mbridge.msdk.** { *; }
+-dontwarn com.mbridge.msdk.**
+-keep interface com.mbridge.msdk.** { *; }
+
+# ---------------------------------------------------------
+# 7. Flutter 通用规则 (确保原生通信不被破坏)
+# ---------------------------------------------------------
+-keep class io.flutter.app.** { *; }
+-keep class io.flutter.plugin.** { *; }
+-keep class io.flutter.util.** { *; }
+-keep class io.flutter.view.** { *; }
+-keep class io.flutter.embedding.** { *; }
+-keep class io.flutter.plugins.** { *; }
+
+# 1. 忽略 Flutter 延迟加载组件引用的 Google Play Core 类
+-dontwarn com.google.android.play.core.**
+
+# 2. 忽略 Ktor 框架引用的 Java 管理类 (ManagementFactory)
+-dontwarn java.lang.management.**
+
+# 3. 忽略 SLF4J 日志库缺失的绑定类
+-dontwarn org.slf4j.impl.**
+
+# 4. 如果你使用了 Ktor 或某些 Kotlin 协程库,建议也加上这个
+-dontwarn io.ktor.**

+ 8 - 0
android/app/src/main/AndroidManifest.xml

@@ -1,11 +1,13 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
     <uses-permission android:name="android.permission.VIBRATE" />
     <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="com.google.android.gms.permission.AD_ID" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
 
     <application
         android:label="Jigsort Solitaire"
         android:name="${applicationName}"
+        android:hardwareAccelerated="true"
         android:icon="@mipmap/launcher_icon">
         <activity
             android:name=".MainActivity"
@@ -34,6 +36,12 @@
         <meta-data
             android:name="flutterEmbedding"
             android:value="2" />
+        <meta-data
+            android:name="com.google.android.gms.ads.APPLICATION_ID"
+            android:value="ca-app-pub-7123620590758627~9715330180"/>
+        <meta-data 
+            android:name="com.facebook.sdk.ApplicationId" 
+            android:value="2126449314427718" />
     </application>
     <!-- Required to query activities that can process text, see:
          https://developer.android.com/training/package-visibility and

+ 1 - 0
android/build.gradle.kts

@@ -2,6 +2,7 @@ allprojects {
     repositories {
         google()
         mavenCentral()
+        maven { url = uri("https://dl-maven-android.mintegral.com/repository/mbridge_android_sdk_oversea") }
     }
 }
 

+ 10 - 2
lib/ads/applovin_ads_controller.dart

@@ -51,6 +51,14 @@ class ApplovinAdsController {
 
     _log.info('AppLovinMAX.initialize...');
 
+    // 1. 开启合规流开关
+    AppLovinMAX.setTermsAndPrivacyPolicyFlowEnabled(true);
+
+    // 2. 设置你的隐私政策和用户协议链接(必须是有效的 URL)
+    AppLovinMAX.setPrivacyPolicyUrl("https://longreachai.net/game/privacy_policy.html");
+    AppLovinMAX.setTermsOfServiceUrl("https://longreachai.net/game/terms_of_service.html");
+
+    // 3. 然后再初始化 SDK
     MaxConfiguration? sdkConfiguration = await AppLovinMAX.initialize(AdHelper.applovinSdkKey);
 
     _log.info('AppLovinMAX.initialize success!');
@@ -134,13 +142,13 @@ class ApplovinAdsController {
     // for ARO : ad_impression 上报给firebase
     FirebaseHelper.logEvent("ad_impression", params);
     // for Taichi
-    FirebaseHelper.logEvent('"Ad_Impression_Revenue', params);
+    FirebaseHelper.logEvent("Ad_Impression_Revenue", params);
 
     // 累计超过0.01 USD 上报 Total_Ads_Revenue_001
     double previousTroasCache = Persistence().tRoasCache;
     double currentTroasCache = previousTroasCache + ad.revenue;
     if (currentTroasCache >= revenueThreshold) {
-      FirebaseHelper.logEvent('"Total_Ads_Revenue_001', {"value": currentTroasCache, "currency": "USD"});
+      FirebaseHelper.logEvent("Total_Ads_Revenue_001", {"value": currentTroasCache, "currency": "USD"});
       Persistence().tRoasCache = 0; // 缓存清零
     } else {
       Persistence().tRoasCache = currentTroasCache;

+ 5 - 3
lib/collection/collection_screen.dart

@@ -11,6 +11,7 @@ import 'package:puzzleweave/models/items.dart';
 import 'package:logging/logging.dart';
 import 'package:lottie/lottie.dart';
 import 'package:provider/provider.dart';
+import 'package:puzzleweave/skin/skin.dart';
 
 final Logger _log = Logger('collection_screen');
 
@@ -87,9 +88,10 @@ class _CollectionScreen extends State<CollectionScreen> {
     final device = context.read<Device>();
     final isTablet = device.isTablet;
     return Scaffold(
+      backgroundColor: SkinHelper.colorWhite,
       appBar: AppBar(
-        backgroundColor: Colors.white,
-        elevation: 1,
+        backgroundColor: SkinHelper.colorWhite,
+        // elevation: 1,
         centerTitle: true,
         leading: IconButton(
           onPressed: () {
@@ -150,7 +152,7 @@ class _CollectionScreen extends State<CollectionScreen> {
     final bool isLocked = index >= data.currentCollectionIndex; // 假设 currentCollectionIndex 之前是解锁的
     return Padding(
       padding: const EdgeInsets.all(10.0),
-      child: GridItem(item: item, lock: isLocked, index: index),
+      child: CollectionGridItem(item: item, lock: isLocked, index: index),
     );
   }
 }

+ 4 - 5
lib/collection/grid_item.dart

@@ -8,19 +8,19 @@ import 'package:puzzleweave/models/items.dart';
 import 'package:puzzleweave/skin/skin.dart';
 import 'package:provider/provider.dart';
 
-class GridItem extends StatefulWidget {
+class CollectionGridItem extends StatefulWidget {
   final ListItem item;
   final bool lock;
   final int index;
-  const GridItem({super.key, required this.item, required this.lock, required this.index});
+  const CollectionGridItem({super.key, required this.item, required this.lock, required this.index});
 
   @override
   State<StatefulWidget> createState() {
-    return _GridItemState();
+    return _CollectionGridItemState();
   }
 }
 
-class _GridItemState extends State<GridItem> {
+class _CollectionGridItemState extends State<CollectionGridItem> {
   @override
   void initState() {
     super.initState();
@@ -82,7 +82,6 @@ class _GridItemState extends State<GridItem> {
             ),
           ),
         ),
-
         Positioned(
           bottom: 0,
           right: 0,

+ 242 - 0
lib/gallery/gallery_screen.dart

@@ -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);
+    }
+  }
+}

+ 122 - 0
lib/gallery/grid_item.dart

@@ -0,0 +1,122 @@
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/material.dart';
+import 'package:fluttertoast/fluttertoast.dart';
+import 'package:puzzleweave/collection/detail_dialog.dart';
+import 'package:puzzleweave/config/device.dart';
+import 'package:puzzleweave/l10n/app_localizations.dart';
+import 'package:puzzleweave/models/items.dart';
+import 'package:puzzleweave/play/board_play.dart';
+import 'package:puzzleweave/skin/skin.dart';
+import 'package:provider/provider.dart';
+
+class GridItem extends StatefulWidget {
+  final ListItem item;
+  final bool lock;
+  final int index;
+  const GridItem({super.key, required this.item, required this.lock, required this.index});
+
+  @override
+  State<StatefulWidget> createState() {
+    return _GridItemState();
+  }
+}
+
+class _GridItemState extends State<GridItem> {
+  @override
+  void initState() {
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      children: [
+        Hero(
+          tag: widget.item.id,
+          child: Material(
+            color: Colors.transparent,
+            shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
+            elevation: 1,
+            clipBehavior: Clip.hardEdge,
+            child: Listener(
+              child: GestureDetector(
+                onTapUp: (details) {
+                  if (widget.lock) {
+                    Fluttertoast.showToast(
+                      msg: AppLocalizations.of(context)!.collectionLocked,
+                      toastLength: Toast.LENGTH_SHORT,
+                      gravity: ToastGravity.CENTER,
+                      timeInSecForIosWeb: 1,
+                      backgroundColor: SkinHelper.slotBorderColor,
+                      textColor: Colors.white,
+                      fontSize: 16.0,
+                    );
+                  } else {
+                    PageRouteBuilder? pageRouteBuilder = BoardPlay.buildRoute(widget.item);
+                    Navigator.push(context, pageRouteBuilder);
+                  }
+                },
+                child: LayoutBuilder(
+                  builder: (context, constraints) {
+                    if (widget.lock) {
+                      return _buildLockedPlaceholder(constraints.biggest);
+                    } else {
+                      return _buildImage(constraints.biggest);
+                    }
+                  },
+                ),
+              ),
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+
+  String get badgeStr => '${widget.index * 25 + 1}-${(widget.index + 1) * 25}';
+
+  Widget _buildImage(Size size) {
+    final device = context.read<Device>();
+    int cacheWidth = (size.width * device.devicePixelRatio).toInt();
+    int cacheHeight = (size.height * device.devicePixelRatio).toInt();
+
+    if (widget.item is AssetItem) {
+      AssetItem assetItem = widget.item as AssetItem;
+      return Image.asset(assetItem.thumb, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
+    } else {
+      return network(widget.item, size.width, size.height, cacheWidth, cacheHeight);
+    }
+  }
+
+  Widget network(ListItem item, double width, double height, int cacheWidth, int cacheHeight) {
+    return CachedNetworkImage(
+      imageUrl: item.thumb,
+      fit: BoxFit.fill,
+      width: width,
+      height: height,
+      memCacheWidth: cacheWidth,
+      memCacheHeight: cacheHeight,
+      // placeholder: (context, url) => JigsawPiece.placeHolder(scale: 0.6),
+    );
+  }
+
+  Widget _buildLockedPlaceholder(Size size) {
+    return Container(
+      width: size.width,
+      height: size.height,
+      decoration: BoxDecoration(color: SkinHelper.slotBorderColor, borderRadius: BorderRadius.circular(8)),
+      child: const Center(
+        child: Icon(
+          Icons.lock,
+          size: 60, // 锁图标大小
+          color: Colors.white, // 锁图标颜色
+        ),
+      ),
+    );
+  }
+}

+ 5 - 2
lib/homepage/home_screen.dart

@@ -3,6 +3,7 @@ import 'dart:io';
 import 'dart:math';
 
 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_messaging/firebase_messaging.dart';
 import 'package:flutter/material.dart';
@@ -342,9 +343,10 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
     }
 
     return Scaffold(
+      backgroundColor: SkinHelper.colorWhite,
       appBar: AppBar(
-        backgroundColor: Colors.white,
-        elevation: 1,
+        backgroundColor: SkinHelper.colorWhite,
+        // elevation: 1,
         centerTitle: true,
         leading: RepaintBoundary(
           // !!! 改造点 3: 添加 ScaleTransition
@@ -381,6 +383,7 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
               audio.playSfx(SfxType.click);
               // Navigator.push(context, SettingsDialog.buildRoute());
               Navigator.push(context, SettingScreen.buildRoute());
+              // AppLovinMAX.showMediationDebugger();
             },
             icon: const Icon(Icons.settings, color: Colors.black87),
           ),

+ 34 - 9
lib/main.dart

@@ -1,3 +1,4 @@
+import 'dart:async';
 import 'dart:developer' as dev;
 import 'dart:io';
 
@@ -76,17 +77,40 @@ void main() async {
       // Pass all uncaught asynchronous errors
       // that aren't handled by the Flutter framework to Crashlytics.
       PlatformDispatcher.instance.onError = (error, stack) {
-        if (error.runtimeType == MissingPluginException) {
-          _log.warning('error=[$error],stack=\n$stack');
-        } else if (error.toString().contains('LoadAdError') ||
-            error.toString().contains('Failed host lookup') ||
-            error.toString().contains('Unable to connect to the server')) {
-          _log.warning('network error: $error[${error.runtimeType}]');
-          // FirebaseCrashlytics.instance.log(error.toString());
-        } else {
-          FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
+        final errorStr = error.toString();
+
+        // 1. 扩展网络/环境类异常的拦截范围
+        final bool isEnvironmentIssue =
+            error is SocketException ||
+            error is HttpException ||
+            error is HandshakeException ||
+            errorStr.contains('ClientException') ||
+            errorStr.contains('SocketException') ||
+            errorStr.contains('HandshakeException') ||
+            errorStr.contains('Failed host lookup') ||
+            errorStr.contains('Network is unreachable') ||
+            errorStr.contains('Connection timed out') ||
+            errorStr.contains('Connection closed') || // 补充:上个日志提到的错误
+            errorStr.contains('Connection reset') || // 补充:被对方重置
+            errorStr.contains('Unable to connect');
+
+        if (isEnvironmentIssue) {
+          _log.warning('已拦截环境异常: $errorStr');
+          FirebaseCrashlytics.instance.log('Env Error (Ignored): $errorStr');
+          return true;
         }
 
+        // 2. 拦截插件缺失类异常
+        if (error is MissingPluginException) {
+          _log.warning('插件未找到: $errorStr');
+          return true;
+        }
+
+        // 3. 剩下的才是逻辑错误(如 Null Check, Range Error)
+        // 改为 fatal: false,避免降低 Google Play 的“崩溃率评分”
+        _log.severe('未处理的逻辑错误', error, stack);
+        FirebaseCrashlytics.instance.recordError(error, stack, fatal: false);
+
         return true;
       };
     } catch (e) {
@@ -164,6 +188,7 @@ class MyApp extends StatelessWidget {
             navigatorObservers: [routeObserver],
             routes: {
               '/': (context) => const HomeScreen(),
+              // '/': (context) => const GalleryScreen(),
               '/play': (context) => BoardPlay(
                 item: AssetItem(
                   cfg.Config.firstId,

+ 2 - 1
lib/rating/rating_dialog.dart

@@ -2,6 +2,7 @@ library rating_dialog;
 
 import 'package:flutter/material.dart';
 import 'package:flutter_rating_bar/flutter_rating_bar.dart';
+import 'package:puzzleweave/skin/skin.dart';
 
 class RatingDialog extends StatefulWidget {
   /// The dialog's title
@@ -129,7 +130,7 @@ class _RatingDialogState extends State<RatingDialog> {
                   padding: const EdgeInsets.all(20),
                   child: ElevatedButton(
                     style: ButtonStyle(
-                      backgroundColor: WidgetStateProperty.all<Color>(const Color(0xff8b4513)),
+                      backgroundColor: WidgetStateProperty.all<Color>(SkinHelper.color5),
                       shape: WidgetStateProperty.all<RoundedRectangleBorder>(RoundedRectangleBorder(borderRadius: BorderRadius.circular(18.0))),
                     ),
                     onPressed: _response!.rating == 0

+ 2 - 1
lib/skin/skin.dart

@@ -7,7 +7,7 @@ class Skin {
   final Color color3;
   final Color color4;
   final Color color5;
-  final Color colorWhite = Colors.white;
+  final Color colorWhite = Color(0xfff4f2e9);
   final Color colorBlack = Colors.black;
 
   Color get wholeBgColor => color1; // 整个游戏背景色
@@ -40,6 +40,7 @@ class SkinHelper {
   static Color get color3 => skins[Persistence().skin].color3;
   static Color get color4 => skins[Persistence().skin].color4;
   static Color get color5 => skins[Persistence().skin].color5;
+  static Color get colorWhite => skins[Persistence().skin].colorWhite;
 
   static Color get wholeBgColor => skins[Persistence().skin].wholeBgColor;
   static Color get coreBgColor => skins[Persistence().skin].coreBgColor;

+ 1 - 1
pubspec.yaml

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 # In Windows, build-name is used as the major, minor, and patch parts
 # of the product and file versions while build-number is used as the build suffix.
-version: 1.0.5+5
+version: 1.0.7+7
 
 environment:
   sdk: ^3.8.1