Parcourir la source

v1.0.8+8 性能优化:解决崩溃,anr,冷启动卡顿等问题,提供阶梯图片质量

guoziyun il y a 4 mois
Parent
commit
e4f124b3e2
66 fichiers modifiés avec 640 ajouts et 482 suppressions
  1. 26 0
      CHANGELOG.md
  2. 4 0
      android/app/src/main/AndroidManifest.xml
  3. BIN
      assets/builtin/691550964b99f02d1db7f6ed.jpeg
  4. BIN
      assets/builtin/691576da4b99f02d1db80e9c.jpeg
  5. BIN
      assets/builtin/691577834b99f02d1db811b3.jpeg
  6. BIN
      assets/builtin/691578314b99f02d1db81309.jpeg
  7. BIN
      assets/builtin/69157cd64b99f02d1db819a5.jpeg
  8. BIN
      assets/builtin/69157ea24b99f02d1db81e14.jpeg
  9. BIN
      assets/builtin/691584ae4b99f02d1db82a1b.jpeg
  10. BIN
      assets/builtin/691585964b99f02d1db82b44.jpeg
  11. BIN
      assets/builtin/6915869d4b99f02d1db82cf2.jpeg
  12. BIN
      assets/builtin/69158aab4b99f02d1db83576.jpeg
  13. BIN
      assets/builtin/691591cf4b99f02d1db84713.jpeg
  14. BIN
      assets/builtin/69174ac74b99f02d1db91783.jpeg
  15. BIN
      assets/builtin/691b4c0d9b45d37f47dc668d.jpeg
  16. BIN
      assets/builtin/691b4c6d9b45d37f47dc66bf.jpeg
  17. BIN
      assets/builtin/691bd6709b45d37f47dc7ad5.jpeg
  18. BIN
      assets/builtin/691bd8589b45d37f47dc7c21.jpeg
  19. BIN
      assets/builtin/691d34ad9b45d37f47dcf31d.jpeg
  20. BIN
      assets/builtin/691d397a9b45d37f47dcf7b8.jpeg
  21. BIN
      assets/builtin/691d3a809b45d37f47dcf8ca.jpeg
  22. BIN
      assets/builtin/691eea1f94c0827052e24617.jpeg
  23. BIN
      assets/builtin/691eebd194c0827052e24a13.jpeg
  24. BIN
      assets/builtin/692529f93c9826728f73c3d4.jpeg
  25. BIN
      assets/builtin/692531f83c9826728f73cb73.jpeg
  26. BIN
      assets/builtin/6926966dede1f42c42e39a69.jpeg
  27. BIN
      assets/builtin/6929d6b7a4a70f6c6369cb5c.jpeg
  28. BIN
      assets/builtin/6929d74ba4a70f6c6369cbb1.jpeg
  29. BIN
      assets/builtin/6929d7e4a4a70f6c6369cc25.jpeg
  30. BIN
      assets/builtin/6956ae9d161be249d4847c1a.jpeg
  31. BIN
      assets/builtin/6956afab161be249d48480d7.jpeg
  32. BIN
      assets/builtin/6956afd0161be249d48481d4.jpeg
  33. BIN
      assets/builtin/6959e000446ba27e12678030.jpeg
  34. BIN
      assets/builtin/6959e02c446ba27e12678148.jpeg
  35. BIN
      assets/builtin/6959e11a446ba27e1267844d.jpeg
  36. BIN
      assets/builtin/695bdd52446ba27e12683cc6.jpeg
  37. BIN
      assets/builtin/695be054446ba27e126843d3.jpeg
  38. BIN
      assets/builtin/695be10f446ba27e12684659.jpeg
  39. BIN
      assets/builtin/695cca5f446ba27e12688d3d.jpeg
  40. BIN
      assets/builtin/695ccb66446ba27e126892aa.jpeg
  41. BIN
      assets/builtin/695ccb8b446ba27e12689309.jpeg
  42. 10 10
      assets/builtin/collection.json
  43. 93 93
      assets/builtin/latest.json
  44. 31 22
      ios/Podfile.lock
  45. 0 8
      ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  46. 0 8
      ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  47. 30 0
      ios/Runner/GoogleService-Info.plist
  48. 51 2
      lib/ads/ads_state.dart
  49. 42 36
      lib/ads/applovin_ads_controller.dart
  50. 35 17
      lib/collection/collection_screen.dart
  51. 2 2
      lib/collection/detail_dialog.dart
  52. 2 2
      lib/collection/grid_item.dart
  53. 33 6
      lib/config/device.dart
  54. 46 16
      lib/gallery/gallery_screen.dart
  55. 2 2
      lib/gallery/grid_item.dart
  56. 3 3
      lib/homepage/home_board.dart
  57. 41 43
      lib/homepage/home_screen.dart
  58. 1 1
      lib/main.dart
  59. 2 2
      lib/models/api_helper.dart
  60. 1 0
      lib/models/cached_request.dart
  61. 39 0
      lib/models/data.dart
  62. 110 175
      lib/models/download.dart
  63. 8 24
      lib/play/board_play.dart
  64. 18 4
      lib/remote_config/remote_config.dart
  65. 3 3
      pubspec.lock
  66. 7 3
      pubspec.yaml

+ 26 - 0
CHANGELOG.md

@@ -0,0 +1,26 @@
+# JcAudioPlayer
+
+JcAudioPlayer 简介
+
+- 播放短音:Android 基于 SoundPool 实现,iOS 基于 AudioToolBox 实现,1.0.1版本开始使用 AvAudioPlayer
+- 播放长音:Android 基于 MediaPlayer 实现,iOS 基于 AvAudioPlayer 实现
+
+注意事项:
+
+- iOS 播放短音基于 AudioToolBox 实现,该原生库播放短音(不超过 30秒)不支持暂停、调节音量的操作,音量调节由"系统提示音"控制。
+- iOS 播放短音如果需要控制暂停、音量等操作,换成 music 接口即可(addMusic、playMusic 等)
+
+## 1.0.0 - 2023-11-13
+
+初版
+
+## 1.0.1 - 2023-11-15
+
+- 修改 iOS 播放短音使用 AvAudioPlayer
+
+## 1.0.3 - 2026-01-28 by guoziyun
+
+- [Android] 引入线程池处理 SoundPool 播放,彻底解决高频触发导致的 ANR。
+- [Android] MediaPlayer 改为异步准备,优化加载性能。
+- [iOS] 引入 AVAudioPlayer 对象池,解决短促音效重叠播放被掐断的问题。
+- [iOS] 优化 AVAudioSession 配置时机,提升稳定性。

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

@@ -9,6 +9,10 @@
         android:name="${applicationName}"
         android:hardwareAccelerated="true"
         android:icon="@mipmap/launcher_icon">
+        <!-- 还是使用skia引擎,稳定性好点-->
+        <meta-data
+            android:name="io.flutter.embedding.android.Renderer"
+            android:value="skia" />
         <activity
             android:name=".MainActivity"
             android:exported="true"

BIN
assets/builtin/691550964b99f02d1db7f6ed.jpeg


BIN
assets/builtin/691576da4b99f02d1db80e9c.jpeg


BIN
assets/builtin/691577834b99f02d1db811b3.jpeg


BIN
assets/builtin/691578314b99f02d1db81309.jpeg


BIN
assets/builtin/69157cd64b99f02d1db819a5.jpeg


BIN
assets/builtin/69157ea24b99f02d1db81e14.jpeg


BIN
assets/builtin/691584ae4b99f02d1db82a1b.jpeg


BIN
assets/builtin/691585964b99f02d1db82b44.jpeg


BIN
assets/builtin/6915869d4b99f02d1db82cf2.jpeg


BIN
assets/builtin/69158aab4b99f02d1db83576.jpeg


BIN
assets/builtin/691591cf4b99f02d1db84713.jpeg


BIN
assets/builtin/69174ac74b99f02d1db91783.jpeg


BIN
assets/builtin/691b4c0d9b45d37f47dc668d.jpeg


BIN
assets/builtin/691b4c6d9b45d37f47dc66bf.jpeg


BIN
assets/builtin/691bd6709b45d37f47dc7ad5.jpeg


BIN
assets/builtin/691bd8589b45d37f47dc7c21.jpeg


BIN
assets/builtin/691d34ad9b45d37f47dcf31d.jpeg


BIN
assets/builtin/691d397a9b45d37f47dcf7b8.jpeg


BIN
assets/builtin/691d3a809b45d37f47dcf8ca.jpeg


BIN
assets/builtin/691eea1f94c0827052e24617.jpeg


BIN
assets/builtin/691eebd194c0827052e24a13.jpeg


BIN
assets/builtin/692529f93c9826728f73c3d4.jpeg


BIN
assets/builtin/692531f83c9826728f73cb73.jpeg


BIN
assets/builtin/6926966dede1f42c42e39a69.jpeg


BIN
assets/builtin/6929d6b7a4a70f6c6369cb5c.jpeg


BIN
assets/builtin/6929d74ba4a70f6c6369cbb1.jpeg


BIN
assets/builtin/6929d7e4a4a70f6c6369cc25.jpeg


BIN
assets/builtin/6956ae9d161be249d4847c1a.jpeg


BIN
assets/builtin/6956afab161be249d48480d7.jpeg


BIN
assets/builtin/6956afd0161be249d48481d4.jpeg


BIN
assets/builtin/6959e000446ba27e12678030.jpeg


BIN
assets/builtin/6959e02c446ba27e12678148.jpeg


BIN
assets/builtin/6959e11a446ba27e1267844d.jpeg


BIN
assets/builtin/695bdd52446ba27e12683cc6.jpeg


BIN
assets/builtin/695be054446ba27e126843d3.jpeg


BIN
assets/builtin/695be10f446ba27e12684659.jpeg


BIN
assets/builtin/695cca5f446ba27e12688d3d.jpeg


BIN
assets/builtin/695ccb66446ba27e126892aa.jpeg


BIN
assets/builtin/695ccb8b446ba27e12689309.jpeg


+ 10 - 10
assets/builtin/collection.json

@@ -3,8 +3,8 @@
     {
       "_id": "691d34ad9b45d37f47dcf31d",
       "title": "Roman",
-      "width": 2000,
-      "height": 3000,
+      "width": 1800,
+      "height": 2700,
       "difficulty": 5,
       "thumb": "assets/builtin/691d34ad9b45d37f47dcf31d.jpeg",
       "raw": "assets/builtin/691d34ad9b45d37f47dcf31d.jpeg"
@@ -12,22 +12,22 @@
     {
       "_id": "691bd6709b45d37f47dc7ad5",
       "title": "Vienna",
-      "width": 2000,
-      "height": 3000,
+      "width": 1800,
+      "height": 2700,
       "difficulty": 5,
       "thumb": "assets/builtin/691bd6709b45d37f47dc7ad5.jpeg",
       "raw": "assets/builtin/691bd6709b45d37f47dc7ad5.jpeg"
     },
     {
-      "_id": "691eebd194c0827052e24a13",
+      "_id": "6929d74ba4a70f6c6369cbb1",
       "title": "Vienna",
-      "width": 2000,
-      "height": 3000,
+      "width": 1800,
+      "height": 2700,
       "difficulty": 5,
-      "thumb": "assets/builtin/691eebd194c0827052e24a13.jpeg",
-      "raw": "assets/builtin/691eebd194c0827052e24a13.jpeg"
+      "thumb": "assets/builtin/6929d74ba4a70f6c6369cbb1.jpeg",
+      "raw": "assets/builtin/6929d74ba4a70f6c6369cbb1.jpeg"
     }
   ],
   "asset": true,
-  "total": 1
+  "total": 3
 }

+ 93 - 93
assets/builtin/latest.json

@@ -2,8 +2,8 @@
   "data": [
     {
       "_id": "6915869d4b99f02d1db82cf2",
-      "width": 2000,
-      "height": 3000,
+      "width": 1800,
+      "height": 2700,
       "difficulty": 3,
       "hard": false,
       "thumb": "assets/builtin/6915869d4b99f02d1db82cf2.jpeg",
@@ -11,176 +11,176 @@
     },
     {
       "_id": "69174ac74b99f02d1db91783",
-      "width": 2000,
-      "height": 3001,
+      "width": 1800,
+      "height": 2700,
       "difficulty": 3,
       "hard": false,
       "thumb": "assets/builtin/69174ac74b99f02d1db91783.jpeg",
       "raw": "assets/builtin/69174ac74b99f02d1db91783.jpeg"
     },
     {
-      "_id": "691576da4b99f02d1db80e9c",
-      "width": 2000,
-      "height": 3000,
+      "_id": "6959e000446ba27e12678030",
+      "width": 1800,
+      "height": 2700,
       "difficulty": 3,
       "hard": false,
-      "thumb": "assets/builtin/691576da4b99f02d1db80e9c.jpeg",
-      "raw": "assets/builtin/691576da4b99f02d1db80e9c.jpeg"
+      "thumb": "assets/builtin/6959e000446ba27e12678030.jpeg",
+      "raw": "assets/builtin/6959e000446ba27e12678030.jpeg"
     },
     {
-      "_id": "692531f83c9826728f73cb73",
-      "width": 2614,
-      "height": 3921,
+      "_id": "695ccb8b446ba27e12689309",
+      "width": 1800,
+      "height": 2700,
       "difficulty": 3,
       "hard": false,
-      "thumb": "assets/builtin/692531f83c9826728f73cb73.jpeg",
-      "raw": "assets/builtin/692531f83c9826728f73cb73.jpeg"
+      "thumb": "assets/builtin/695ccb8b446ba27e12689309.jpeg",
+      "raw": "assets/builtin/695ccb8b446ba27e12689309.jpeg"
     },
     {
-      "_id": "691b4c0d9b45d37f47dc668d",
-      "width": 2000,
-      "height": 3000,
+      "_id": "6959e02c446ba27e12678148",
+      "width": 1800,
+      "height": 2700,
       "difficulty": 3,
       "hard": false,
-      "thumb": "assets/builtin/691b4c0d9b45d37f47dc668d.jpeg",
-      "raw": "assets/builtin/691b4c0d9b45d37f47dc668d.jpeg"
+      "thumb": "assets/builtin/6959e02c446ba27e12678148.jpeg",
+      "raw": "assets/builtin/6959e02c446ba27e12678148.jpeg"
     },
     {
-      "_id": "691550964b99f02d1db7f6ed",
-      "width": 2000,
-      "height": 3000,
+      "_id": "691b4c6d9b45d37f47dc66bf",
+      "width": 1800,
+      "height": 2700,
       "difficulty": 3,
       "hard": false,
-      "thumb": "assets/builtin/691550964b99f02d1db7f6ed.jpeg",
-      "raw": "assets/builtin/691550964b99f02d1db7f6ed.jpeg"
+      "thumb": "assets/builtin/691b4c6d9b45d37f47dc66bf.jpeg",
+      "raw": "assets/builtin/691b4c6d9b45d37f47dc66bf.jpeg"
     },
     {
-      "_id": "691577834b99f02d1db811b3",
-      "width": 2000,
-      "height": 3000,
+      "_id": "6929d7e4a4a70f6c6369cc25",
+      "width": 1800,
+      "height": 2700,
       "difficulty": 3,
       "hard": false,
-      "thumb": "assets/builtin/691577834b99f02d1db811b3.jpeg",
-      "raw": "assets/builtin/691577834b99f02d1db811b3.jpeg"
+      "thumb": "assets/builtin/6929d7e4a4a70f6c6369cc25.jpeg",
+      "raw": "assets/builtin/6929d7e4a4a70f6c6369cc25.jpeg"
     },
     {
-      "_id": "69157cd64b99f02d1db819a5",
-      "width": 2000,
-      "height": 3000,
+      "_id": "6956afab161be249d48480d7",
+      "width": 1800,
+      "height": 2700,
       "difficulty": 3,
       "hard": false,
-      "thumb": "assets/builtin/69157cd64b99f02d1db819a5.jpeg",
-      "raw": "assets/builtin/69157cd64b99f02d1db819a5.jpeg"
+      "thumb": "assets/builtin/6956afab161be249d48480d7.jpeg",
+      "raw": "assets/builtin/6956afab161be249d48480d7.jpeg"
     },
     {
       "_id": "69158aab4b99f02d1db83576",
-      "width": 2000,
-      "height": 3000,
+      "width": 1800,
+      "height": 2700,
       "difficulty": 4,
       "hard": true,
       "thumb": "assets/builtin/69158aab4b99f02d1db83576.jpeg",
       "raw": "assets/builtin/69158aab4b99f02d1db83576.jpeg"
     },
     {
-      "_id": "691591cf4b99f02d1db84713",
-      "width": 2000,
-      "height": 3000,
+      "_id": "6959e11a446ba27e1267844d",
+      "width": 1800,
+      "height": 2700,
       "difficulty": 3,
       "hard": false,
-      "thumb": "assets/builtin/691591cf4b99f02d1db84713.jpeg",
-      "raw": "assets/builtin/691591cf4b99f02d1db84713.jpeg"
+      "thumb": "assets/builtin/6959e11a446ba27e1267844d.jpeg",
+      "raw": "assets/builtin/6959e11a446ba27e1267844d.jpeg"
     },
     {
-      "_id": "6929d6b7a4a70f6c6369cb5c",
-      "width": 2261,
-      "height": 3392,
+      "_id": "6956afd0161be249d48481d4",
+      "width": 1800,
+      "height": 2700,
       "difficulty": 3,
       "hard": false,
-      "thumb": "assets/builtin/6929d6b7a4a70f6c6369cb5c.jpeg",
-      "raw": "assets/builtin/6929d6b7a4a70f6c6369cb5c.jpeg"
+      "thumb": "assets/builtin/6956afd0161be249d48481d4.jpeg",
+      "raw": "assets/builtin/6956afd0161be249d48481d4.jpeg"
     },
     {
-      "_id": "691585964b99f02d1db82b44",
-      "width": 2000,
-      "height": 3000,
+      "_id": "692529f93c9826728f73c3d4",
+      "width": 1800,
+      "height": 2700,
       "difficulty": 3,
       "hard": false,
-      "thumb": "assets/builtin/691585964b99f02d1db82b44.jpeg",
-      "raw": "assets/builtin/691585964b99f02d1db82b44.jpeg"
+      "thumb": "assets/builtin/692529f93c9826728f73c3d4.jpeg",
+      "raw": "assets/builtin/692529f93c9826728f73c3d4.jpeg"
     },
     {
-      "_id": "691d3a809b45d37f47dcf8ca",
-      "width": 2000,
-      "height": 3000,
+      "_id": "695be054446ba27e126843d3",
+      "width": 1800,
+      "height": 2700,
       "difficulty": 4,
       "hard": true,
-      "thumb": "assets/builtin/691d3a809b45d37f47dcf8ca.jpeg",
-      "raw": "assets/builtin/691d3a809b45d37f47dcf8ca.jpeg"
+      "thumb": "assets/builtin/695be054446ba27e126843d3.jpeg",
+      "raw": "assets/builtin/695be054446ba27e126843d3.jpeg"
     },
     {
-      "_id": "691584ae4b99f02d1db82a1b",
-      "width": 2000,
-      "height": 3000,
+      "_id": "695cca5f446ba27e12688d3d",
+      "width": 1800,
+      "height": 2700,
       "difficulty": 3,
       "hard": false,
-      "thumb": "assets/builtin/691584ae4b99f02d1db82a1b.jpeg",
-      "raw": "assets/builtin/691584ae4b99f02d1db82a1b.jpeg"
+      "thumb": "assets/builtin/695cca5f446ba27e12688d3d.jpeg",
+      "raw": "assets/builtin/695cca5f446ba27e12688d3d.jpeg"
     },
     {
-      "_id": "691d397a9b45d37f47dcf7b8",
-      "width": 2000,
-      "height": 3000,
+      "_id": "691bd8589b45d37f47dc7c21",
+      "width": 1800,
+      "height": 2700,
       "difficulty": 3,
       "hard": false,
-      "thumb": "assets/builtin/691d397a9b45d37f47dcf7b8.jpeg",
-      "raw": "assets/builtin/691d397a9b45d37f47dcf7b8.jpeg"
-    },
-    {
-      "_id": "691bd8589b45d37f47dc7c21",
-      "width": 2000,
-      "height": 3000,
-      "difficulty": 4,
-      "hard": true,
       "thumb": "assets/builtin/691bd8589b45d37f47dc7c21.jpeg",
       "raw": "assets/builtin/691bd8589b45d37f47dc7c21.jpeg"
     },
     {
-      "_id": "691eea1f94c0827052e24617",
-      "width": 3109,
-      "height": 4664,
+      "_id": "6956ae9d161be249d4847c1a",
+      "width": 1800,
+      "height": 2700,
       "difficulty": 4,
-      "hard": false,
-      "thumb": "assets/builtin/691eea1f94c0827052e24617.jpeg",
-      "raw": "assets/builtin/691eea1f94c0827052e24617.jpeg"
+      "hard": true,
+      "thumb": "assets/builtin/6956ae9d161be249d4847c1a.jpeg",
+      "raw": "assets/builtin/6956ae9d161be249d4847c1a.jpeg"
     },
     {
-      "_id": "6926966dede1f42c42e39a69",
-      "width": 4480,
-      "height": 6720,
+      "_id": "695ccb66446ba27e126892aa",
+      "width": 1800,
+      "height": 2700,
       "difficulty": 4,
       "hard": false,
-      "thumb": "assets/builtin/6926966dede1f42c42e39a69.jpeg",
-      "raw": "assets/builtin/6926966dede1f42c42e39a69.jpeg"
+      "thumb": "assets/builtin/695ccb66446ba27e126892aa.jpeg",
+      "raw": "assets/builtin/695ccb66446ba27e126892aa.jpeg"
     },
     {
       "_id": "69157ea24b99f02d1db81e14",
-      "width": 2000,
-      "height": 3000,
+      "width": 1800,
+      "height": 2700,
       "difficulty": 5,
-      "hard": true,
+      "hard": false,
       "thumb": "assets/builtin/69157ea24b99f02d1db81e14.jpeg",
       "raw": "assets/builtin/69157ea24b99f02d1db81e14.jpeg"
     },
     {
-      "_id": "691578314b99f02d1db81309",
-      "width": 2000,
-      "height": 3000,
+      "_id": "695be10f446ba27e12684659",
+      "width": 1800,
+      "height": 2700,
+      "difficulty": 3,
+      "hard": true,
+      "thumb": "assets/builtin/695be10f446ba27e12684659.jpeg",
+      "raw": "assets/builtin/695be10f446ba27e12684659.jpeg"
+    },
+    {
+      "_id": "695bdd52446ba27e12683cc6",
+      "width": 1800,
+      "height": 2700,
       "difficulty": 3,
       "hard": false,
-      "thumb": "assets/builtin/691578314b99f02d1db81309.jpeg",
-      "raw": "assets/builtin/691578314b99f02d1db81309.jpeg"
+      "thumb": "assets/builtin/695bdd52446ba27e12683cc6.jpeg",
+      "raw": "assets/builtin/695bdd52446ba27e12683cc6.jpeg"
     }
   ],
   "asset": true,
-  "total": 9
+  "total": 20
 }

+ 31 - 22
ios/Podfile.lock

@@ -1,15 +1,18 @@
 PODS:
-  - advertising_id (0.0.1):
+  - Adjust (5.5.0):
+    - Adjust/Adjust (= 5.5.0)
+  - Adjust/Adjust (5.5.0):
+    - AdjustSignature (= 3.62.0)
+  - adjust_sdk (5.5.0):
+    - Adjust (= 5.5.0)
     - Flutter
+  - AdjustSignature (3.62.0)
   - app_tracking_transparency (0.0.1):
     - Flutter
-  - applovin_max (4.6.1):
-    - AppLovinSDK (= 13.5.1)
+  - applovin_max (3.11.1):
+    - AppLovinSDK (= 12.6.1)
     - Flutter
-  - AppLovinSDK (13.5.1)
-  - audioplayers_darwin (0.0.1):
-    - Flutter
-    - FlutterMacOS
+  - AppLovinSDK (12.6.1)
   - device_info_plus (0.0.1):
     - Flutter
   - Firebase/CoreOnly (12.4.0):
@@ -113,6 +116,8 @@ PODS:
     - PromisesSwift (~> 2.1)
   - FirebaseSharedSwift (12.4.0)
   - Flutter (1.0.0)
+  - flutter_native_splash (2.4.3):
+    - Flutter
   - fluttertoast (0.0.2):
     - Flutter
   - GoogleAdsOnDeviceConversion (3.1.0):
@@ -174,7 +179,7 @@ PODS:
     - GoogleUtilities/Privacy
   - jc_audio_player (0.0.1):
     - Flutter
-  - launch_review (0.0.1):
+  - launch_review_latest (0.0.1):
     - Flutter
   - nanopb (3.30910.0):
     - nanopb/decode (= 3.30910.0)
@@ -206,10 +211,9 @@ PODS:
     - Flutter
 
 DEPENDENCIES:
-  - advertising_id (from `.symlinks/plugins/advertising_id/ios`)
+  - adjust_sdk (from `.symlinks/plugins/adjust_sdk/ios`)
   - app_tracking_transparency (from `.symlinks/plugins/app_tracking_transparency/ios`)
   - applovin_max (from `.symlinks/plugins/applovin_max/ios`)
-  - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
   - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
   - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
@@ -217,9 +221,10 @@ DEPENDENCIES:
   - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
   - firebase_remote_config (from `.symlinks/plugins/firebase_remote_config/ios`)
   - Flutter (from `Flutter`)
+  - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
   - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
   - jc_audio_player (from `.symlinks/plugins/jc_audio_player/ios`)
-  - launch_review (from `.symlinks/plugins/launch_review/ios`)
+  - launch_review_latest (from `.symlinks/plugins/launch_review_latest/ios`)
   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
   - rate_my_app (from `.symlinks/plugins/rate_my_app/darwin`)
@@ -231,6 +236,8 @@ DEPENDENCIES:
 
 SPEC REPOS:
   trunk:
+    - Adjust
+    - AdjustSignature
     - AppLovinSDK
     - Firebase
     - FirebaseABTesting
@@ -254,14 +261,12 @@ SPEC REPOS:
     - PromisesSwift
 
 EXTERNAL SOURCES:
-  advertising_id:
-    :path: ".symlinks/plugins/advertising_id/ios"
+  adjust_sdk:
+    :path: ".symlinks/plugins/adjust_sdk/ios"
   app_tracking_transparency:
     :path: ".symlinks/plugins/app_tracking_transparency/ios"
   applovin_max:
     :path: ".symlinks/plugins/applovin_max/ios"
-  audioplayers_darwin:
-    :path: ".symlinks/plugins/audioplayers_darwin/darwin"
   device_info_plus:
     :path: ".symlinks/plugins/device_info_plus/ios"
   firebase_analytics:
@@ -276,12 +281,14 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/firebase_remote_config/ios"
   Flutter:
     :path: Flutter
+  flutter_native_splash:
+    :path: ".symlinks/plugins/flutter_native_splash/ios"
   fluttertoast:
     :path: ".symlinks/plugins/fluttertoast/ios"
   jc_audio_player:
     :path: ".symlinks/plugins/jc_audio_player/ios"
-  launch_review:
-    :path: ".symlinks/plugins/launch_review/ios"
+  launch_review_latest:
+    :path: ".symlinks/plugins/launch_review_latest/ios"
   package_info_plus:
     :path: ".symlinks/plugins/package_info_plus/ios"
   path_provider_foundation:
@@ -300,11 +307,12 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/vibration/ios"
 
 SPEC CHECKSUMS:
-  advertising_id: d5de9e659986092d7ca50977dc50f4f4fcd4c30a
+  Adjust: a0095eda439b925f809b0efbf64923c05701e937
+  adjust_sdk: 3dde244a99742bcff3f87d3ea2c78eb567250d64
+  AdjustSignature: 682d788005a6e21557913a9dc0381ae8d943ca0a
   app_tracking_transparency: 3d84f147f67ca82d3c15355c36b1fa6b66ca7c92
-  applovin_max: 418fe6721301e35d80ad7d8c492b05dd7fb72cd8
-  AppLovinSDK: 8d9af6c7617e4d35b38aa8393a3e9d161a74b230
-  audioplayers_darwin: 4f9ca89d92d3d21cec7ec580e78ca888e5fb68bd
+  applovin_max: 7be829d3fe1447bd5b6681bc3a283c41485974fa
+  AppLovinSDK: a892bbeff744749a8121bd863aa1399f7eef6ef1
   device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
   Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
   firebase_analytics: 67fbdd9f3c04e55048024f3da21cfc36f05e56cf
@@ -325,13 +333,14 @@ SPEC CHECKSUMS:
   FirebaseSessions: ba7c7a7ca8696a8d540eb3fe3800fbe98c79786d
   FirebaseSharedSwift: 93426a1de92f19e1199fac5295a4f8df16458daa
   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
+  flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
   fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
   GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1
   GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d
   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
   jc_audio_player: 351f7fab00cb001260973d51005a9f4f0a120d87
-  launch_review: ffa7f5f539f248166db58a441664b5db6df4e09c
+  launch_review_latest: 28e236fc255d91ec1430c39f951c4db1398c79c1
   nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
   package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
   path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564

+ 0 - 8
ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>IDEDidComputeMac32BitWarning</key>
-	<true/>
-</dict>
-</plist>

+ 0 - 8
ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>PreviewsEnabled</key>
-	<false/>
-</dict>
-</plist>

+ 30 - 0
ios/Runner/GoogleService-Info.plist

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>API_KEY</key>
+	<string>AIzaSyBRbzz8F7u4uIzQV6nqybwKI9XawIlADK0</string>
+	<key>GCM_SENDER_ID</key>
+	<string>1034630421426</string>
+	<key>PLIST_VERSION</key>
+	<string>1</string>
+	<key>BUNDLE_ID</key>
+	<string>jigsort.solitaire.jigsaw.match.games</string>
+	<key>PROJECT_ID</key>
+	<string>jigsort-186f6</string>
+	<key>STORAGE_BUCKET</key>
+	<string>jigsort-186f6.firebasestorage.app</string>
+	<key>IS_ADS_ENABLED</key>
+	<false></false>
+	<key>IS_ANALYTICS_ENABLED</key>
+	<false></false>
+	<key>IS_APPINVITE_ENABLED</key>
+	<true></true>
+	<key>IS_GCM_ENABLED</key>
+	<true></true>
+	<key>IS_SIGNIN_ENABLED</key>
+	<true></true>
+	<key>GOOGLE_APP_ID</key>
+	<string>1:1034630421426:android:735a56b7d9b13238f322ab</string>
+</dict>
+</plist>

+ 51 - 2
lib/ads/ads_state.dart

@@ -6,6 +6,7 @@ import 'package:logging/logging.dart';
 import 'package:provider/provider.dart';
 import 'package:puzzleweave/ads/applovin_ads_controller.dart';
 import 'package:puzzleweave/l10n/app_localizations.dart';
+import 'package:puzzleweave/models/data.dart';
 import 'package:puzzleweave/persistence/persistence.dart';
 import 'package:puzzleweave/skin/skin.dart';
 
@@ -26,6 +27,8 @@ abstract class AdsState<T extends StatefulWidget> extends State<T> {
   Function(AdState state)? onRewardAdState; // reward广告状态回调,外部如关心,可设置此回调
   Function(AdState state)? onInterstitialAdState; // reward广告状态回调,外部如关心,可设置此回调
 
+  late Data data;
+
   late ApplovinAdsController _applovinAdsController;
   late ValueNotifier<AppLifecycleState> lifecycleNotifier;
 
@@ -46,9 +49,19 @@ abstract class AdsState<T extends StatefulWidget> extends State<T> {
   //激励广告当前状态
   AdState get rewardState => _applovinAdsController.rewardedAdState.value;
 
+  // 新增:用于控制 Banner 实际显示的变量
+  bool _isBannerVisible = false;
+  bool get isBannerVisible => _isBannerVisible;
+
   @override
   void initState() {
     super.initState();
+
+    data = context.read<Data>();
+    // 1. 监听 ValueNotifier 的变化
+    // 当 completedWorks.value 被重新赋值时(见 data.dart 的 workDone 方法),触发刷新
+    data.completedWorks.addListener(_onLevelChanged);
+
     _applovinAdsController = context.read<ApplovinAdsController>();
     _applovinAdsController.interstitialAdState.removeListener(_onInterstitialAdState);
     _applovinAdsController.interstitialAdState.addListener(_onInterstitialAdState);
@@ -65,10 +78,42 @@ abstract class AdsState<T extends StatefulWidget> extends State<T> {
 
     _applovinAdsController.loadInterstitialAd();
     _applovinAdsController.loadRewardedAd();
+
+    // 初始检查一次 Banner
+    _updateBannerVisibility();
+  }
+
+  // 关卡变化的回调
+  void _onLevelChanged() {
+    _log.info("Level changed detected via ValueNotifier, updating banner...");
+    _updateBannerVisibility();
+  }
+
+  // 核心改进:主动更新 Banner 状态
+  Future<void> _updateBannerVisibility() async {
+    if (!mounted) return;
+
+    // 1. 确保 SDK 已初始化
+    bool ready = await _applovinAdsController.completer.future;
+
+    // 2. 获取当前关卡数
+    int doneLevels = data.currentLevel;
+
+    // 3. 综合判断:SDK就绪 + 逻辑允许 + 应用在前台
+    bool shouldShow = ready && shouldShowBannerAd(doneLevels) && lifecycleNotifier.value == AppLifecycleState.resumed;
+
+    if (_isBannerVisible != shouldShow) {
+      setState(() {
+        _isBannerVisible = shouldShow;
+      });
+      _log.info("Banner visibility updated: $_isBannerVisible");
+    }
   }
 
   @override
   void dispose() {
+    data.completedWorks.removeListener(_onLevelChanged);
+
     intersReadyNotifier.dispose();
     rewardReadyNotifier.dispose();
 
@@ -110,8 +155,8 @@ abstract class AdsState<T extends StatefulWidget> extends State<T> {
   }
 
   /// banner 广告
-  Widget get adBanner {
-    return _applovinAdsController.bannerAdWidget;
+  Widget getBanner(String positionKey) {
+    return _applovinAdsController.getBannerWidget(positionKey);
   }
 
   ///显示激励广告
@@ -245,6 +290,10 @@ abstract class AdsState<T extends StatefulWidget> extends State<T> {
     _log.info('AppLifecycleState changed: ${lifecycleNotifier.value}');
     lifeState = lifecycleNotifier.value;
 
+    // 当回到前台时,重新检查 Banner;切到后台时,理论上 MaxAdView 会自动处理,
+    // 但我们可以通过 setState 强制隐藏它来确保安全。
+    _updateBannerVisibility();
+
     if (lifeState == AppLifecycleState.inactive) {
       //前台可见,但是无法交互
       onInactive();

+ 42 - 36
lib/ads/applovin_ads_controller.dart

@@ -93,42 +93,48 @@ class ApplovinAdsController {
     // AppLovinMAX.createBanner(AdHelper.applovinBannerAdUnitId, AdViewPosition.bottomCenter);
   }
 
-  Widget get bannerAdWidget => MaxAdView(
-    adUnitId: AdHelper.applovinBannerAdUnitId,
-    adFormat: AdFormat.banner,
-    placement: 'banner',
-    extraParameters: const {'adaptive_banner': 'true'},
-    listener: AdViewAdListener(
-      onAdLoadedCallback: (ad) {
-        // // 广告加载成功后, ad.size 包含实际的宽度和高度 (dp)
-        // double? widthDp = ad.size?.width;
-        // double? heightDp = ad.size?.height;
-        // if (heightDp != null) {
-        //   context.read<Device>().bannerHeight = heightDp;
-        // }
-        // _log.info('banner广告 width = $widthDp, height = $heightDp');
-        _log.info(() => 'applovin banner Ad loaded: ${ad.hashCode}');
-      },
-      onAdLoadFailedCallback: (adUnitId, error) {
-        _log.warning('applovin banner Ad failedToLoad: $error');
-      },
-      onAdClickedCallback: (ad) {
-        _log.info('applovin banner Ad click registered');
-      },
-      onAdExpandedCallback: (ad) {
-        _log.info('applovin banner Ad expanded');
-      },
-      onAdCollapsedCallback: (ad) {
-        _log.info('applovin banner Ad collaspsed');
-      },
-      onAdRevenuePaidCallback: (ad) {
-        _log.info('woooooooooo, applovin banner paid event: revenue: ${ad.revenue} precision: ${ad.revenuePrecision}');
-        if (ad.revenue > 0) {
-          onAdRevenuePaid(ad); // revenue 单位转化为 valueMicro,以便和admod一致
-        }
-      },
-    ),
-  );
+  Widget getBannerWidget(String positionKey) {
+    // 如果调用时 SDK 还没初始化完成,返回空,避免 Native 层空指针
+    if (!_hasInit) return const SizedBox.shrink();
+
+    return MaxAdView(
+      key: ValueKey(positionKey), // 只要 positionKey 不变,页面内刷新就不会重建
+      adUnitId: AdHelper.applovinBannerAdUnitId,
+      adFormat: AdFormat.banner,
+      placement: 'banner',
+      extraParameters: const {'adaptive_banner': 'true'},
+      listener: AdViewAdListener(
+        onAdLoadedCallback: (ad) {
+          // // 广告加载成功后, ad.size 包含实际的宽度和高度 (dp)
+          // double? widthDp = ad.size?.width;
+          // double? heightDp = ad.size?.height;
+          // if (heightDp != null) {
+          //   context.read<Device>().bannerHeight = heightDp;
+          // }
+          // _log.info('banner广告 width = $widthDp, height = $heightDp');
+          _log.info(() => 'applovin banner Ad loaded: ${ad.hashCode}');
+        },
+        onAdLoadFailedCallback: (adUnitId, error) {
+          _log.warning('applovin banner Ad failedToLoad: $error');
+        },
+        onAdClickedCallback: (ad) {
+          _log.info('applovin banner Ad click registered');
+        },
+        onAdExpandedCallback: (ad) {
+          _log.info('applovin banner Ad expanded');
+        },
+        onAdCollapsedCallback: (ad) {
+          _log.info('applovin banner Ad collaspsed');
+        },
+        onAdRevenuePaidCallback: (ad) {
+          _log.info('woooooooooo, applovin banner paid event: revenue: ${ad.revenue} precision: ${ad.revenuePrecision}');
+          if (ad.revenue > 0) {
+            onAdRevenuePaid(ad); // revenue 单位转化为 valueMicro,以便和admod一致
+          }
+        },
+      ),
+    );
+  }
 
   // revenue 回调处理,所有广告类型统一在此处理
   onAdRevenuePaid(MaxAd ad) async {

+ 35 - 17
lib/collection/collection_screen.dart

@@ -127,24 +127,42 @@ class _CollectionScreen extends State<CollectionScreen> {
     );
   }
 
-  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 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 get scrollableDummy => Scaffold(
+    backgroundColor: SkinHelper.colorWhite,
+    body: Center(
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          // 替换 Lottie 为原生进度条
+          SizedBox(width: 40, height: 40, child: CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation<Color>(SkinHelper.coreBgColor))),
+          const SizedBox(height: 16),
+          Text(
+            "Loading...",
+            style: TextStyle(color: SkinHelper.slotBorderColor.withOpacity(0.7), fontSize: 14, fontWeight: FontWeight.w500),
           ),
-        ),
-      );
-    },
+        ],
+      ),
+    ),
   );
 
   Widget _buildItem(context, index) {

+ 2 - 2
lib/collection/detail_dialog.dart

@@ -35,13 +35,13 @@ class _ImageDetailDialog extends State<ImageDetailDialog> {
   void loadImage() async {
     try {
       Device device = context.read<Device>();
-      double dpr = device.devicePixelRatio;
+      double dpr = device.effectivePixelRatio;
 
       // 计算最佳尺寸(屏幕宽度90%,按2:3比例计算高度)
       final bestWidth = (device.screenSize.width * 0.9 * dpr).round();
       final bestHeight = (bestWidth * 3 / 2).round();
 
-      ItemLoader itemLoader = ItemLoader.load(widget.item);
+      ItemLoader itemLoader = ItemLoader.load(widget.item, device.suggestedQuality);
       final loadedImage = await itemLoader.getImageBySize(bestWidth, bestHeight);
 
       if (mounted) {

+ 2 - 2
lib/collection/grid_item.dart

@@ -107,8 +107,8 @@ class _CollectionGridItemState extends State<CollectionGridItem> {
 
   Widget _buildImage(Size size) {
     final device = context.read<Device>();
-    int cacheWidth = (size.width * device.devicePixelRatio).toInt();
-    int cacheHeight = (size.height * device.devicePixelRatio).toInt();
+    int cacheWidth = (size.width * device.realPixelRatio).toInt();
+    int cacheHeight = (size.height * device.realPixelRatio).toInt();
 
     if (widget.item is AssetItem) {
       AssetItem assetItem = widget.item as AssetItem;

+ 33 - 6
lib/config/device.dart

@@ -14,11 +14,24 @@ class Device {
 
   AndroidDeviceInfo? androidDeviceInfo;
 
+  // 新增:判断是否32位设备
+  bool is32BitDevice() {
+    if (Platform.isIOS) return false; // 近年iOS全是64位
+    if (androidDeviceInfo == null) return false;
+    final abis = androidDeviceInfo!.supportedAbis;
+    return !abis.any((abi) => abi.contains('64'));
+  }
+
   /// 获取平台性能
   int get androidSdkInt => androidDeviceInfo != null ? androidDeviceInfo!.version.sdkInt : 100;
   bool get isOldAndroid => androidSdkInt < 26; // 安卓8以下
   bool get isLowRamDevice => androidDeviceInfo != null ? androidDeviceInfo!.isLowRamDevice : false;
-  bool get isLowEndDevice => Platform.numberOfProcessors <= 1 || isOldAndroid || isLowRamDevice;
+  bool get lowCpu => Platform.numberOfProcessors <= 2;
+
+  bool get isLowEndDevice {
+    if (androidDeviceInfo == null) return false;
+    return lowCpu || isOldAndroid || isLowRamDevice || is32BitDevice();
+  }
 
   static double get devPixelRatio => PlatformDispatcher.instance.views.first.devicePixelRatio;
   static Size get physicalSize => PlatformDispatcher.instance.views.first.physicalSize;
@@ -35,8 +48,6 @@ class Device {
 
   double _bannerHeight = 0;
 
-  int get previewImageSize => (boardSize.shortestSide * devicePixelRatio).toInt();
-
   set bannerHeight(double height) {
     _bannerHeight = height;
   }
@@ -47,9 +58,25 @@ class Device {
   /// 屏幕尺寸
   Size get screenSize => MediaQuery.of(context).size;
 
+  // 真实像素密度
+  double get realPixelRatio => devPixelRatio;
+
   /// 像素密度
-  double get devicePixelRatio => isLowEndDevice ? 1 : MediaQuery.of(context).devicePixelRatio;
-  double get dpi => devicePixelRatio * 160;
+  /// 像素密度:低端机通过降低采样倍率来保护内存
+  /// 这里的逻辑是:高端机用真实DPR,低端机降级,但不直接降到1(除非设备极差)
+  double get effectivePixelRatio {
+    double realDPR = PlatformDispatcher.instance.views.first.devicePixelRatio;
+    if (isLowEndDevice) {
+      return (realDPR > 2.0) ? 1.5 : 1.0; // 适当降级,保留一点清晰度
+    }
+    return realDPR;
+  }
+
+  String get suggestedQuality {
+    if (isLowEndDevice) return "1200";
+    if (isTablet) return "2400"; // 平板需要更高像素
+    return "1800";
+  }
 
   /// safeArea高度 Z
   double get safeAreaHeight => MediaQuery.of(context).viewPadding.top + MediaQuery.of(context).viewPadding.bottom;
@@ -105,7 +132,7 @@ class Device {
   }
 
   // 最佳图片分辨率
-  Size get bestImageSize => Size(targetRect.width * devPixelRatio, targetRect.height * devPixelRatio);
+  Size get bestImageSize => Size(targetRect.width * effectivePixelRatio, targetRect.height * effectivePixelRatio);
 }
 
 class DeviceProfile {

+ 46 - 16
lib/gallery/gallery_screen.dart

@@ -17,6 +17,7 @@ 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';
+import 'package:puzzleweave/skin/skin.dart';
 
 final Logger _log = Logger('gallery_screen');
 
@@ -92,6 +93,7 @@ class _GalleryScreen extends State<GalleryScreen> {
     final isTablet = device.isTablet;
 
     return Scaffold(
+      backgroundColor: SkinHelper.colorWhite,
       body: latest == null
           ? scrollableDummy
           : RefreshIndicator(
@@ -113,24 +115,52 @@ class _GalleryScreen extends State<GalleryScreen> {
     );
   }
 
-  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 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 get scrollableDummy => Scaffold(
+    backgroundColor: SkinHelper.colorWhite, // 确保背景色统一
+    body: Center(
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          // 替换 Lottie 为原生轻量级进度条
+          SizedBox(
+            width: 40,
+            height: 40,
+            child: CircularProgressIndicator(
+              strokeWidth: 3,
+              valueColor: AlwaysStoppedAnimation<Color>(
+                // 使用你定义的 SkinHelper 核心色,如果没有则用黑色
+                SkinHelper.coreBgColor,
+              ),
             ),
           ),
-        ),
-      );
-    },
+          const SizedBox(height: 20),
+          Text(
+            "Loading...",
+            style: TextStyle(color: SkinHelper.slotBorderColor.withOpacity(0.7), fontSize: 14, fontWeight: FontWeight.w500),
+          ),
+        ],
+      ),
+    ),
   );
 
   Widget _buildItem(context, index) {

+ 2 - 2
lib/gallery/grid_item.dart

@@ -82,8 +82,8 @@ class _GridItemState extends State<GridItem> {
 
   Widget _buildImage(Size size) {
     final device = context.read<Device>();
-    int cacheWidth = (size.width * device.devicePixelRatio).toInt();
-    int cacheHeight = (size.height * device.devicePixelRatio).toInt();
+    int cacheWidth = (size.width * device.realPixelRatio).toInt();
+    int cacheHeight = (size.height * device.realPixelRatio).toInt();
 
     if (widget.item is AssetItem) {
       AssetItem assetItem = widget.item as AssetItem;

+ 3 - 3
lib/homepage/home_board.dart

@@ -86,8 +86,8 @@ class HomeBoard {
     // _clearImage();
 
     try {
-      double dpr = device.devicePixelRatio;
-      ItemLoader itemLoader = ItemLoader.load(_currentCollectionItem!);
+      double dpr = device.effectivePixelRatio;
+      ItemLoader itemLoader = ItemLoader.load(_currentCollectionItem!, device.suggestedQuality);
 
       // 异步获取图片
       ui.Image? loadedImage = await itemLoader.getImageBySize((canvasWidth * dpr).round(), (canvasHeight * dpr).round());
@@ -118,7 +118,7 @@ class HomeBoard {
   // 5. 修改:安全加载卡片背面图
   Future<void> _loadCardImage() async {
     try {
-      double dpr = device.devicePixelRatio;
+      double dpr = device.realPixelRatio;
       final Size bestCardSize = Size(pieceLogicalWidth * dpr, pieceLogicalHeight * dpr);
 
       final ByteData cardData = await rootBundle.load('assets/images/backcard_green.png');

+ 41 - 43
lib/homepage/home_screen.dart

@@ -177,13 +177,8 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
           initThird();
         }
 
-        // !!! 核心修改:只有在数据完整且最近网络请求成功时,才启动预加载
-        if (isNetworkActive) {
-          _log.info('Data sufficient AND Network Active. Starting preload.');
-          Future.delayed(const Duration(seconds: 3), () => _preloadNextImages());
-        } else {
-          // 数据完整,但来自缓存,网络状态未知,3秒后尝试刷新(refresh)
-          _log.info('Data sufficient BUT Network status unknown/inactive. Attempting refresh in 3s.');
+        if (!isNetworkActive) {
+          // 如果是从缓存读取的,网络状态未知,静默刷新列表即可,不触发图片下载
           Future.delayed(Duration(seconds: 3), () => refresh());
         }
       } else {
@@ -252,7 +247,7 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
   }
 
   /// 预加载未来 N 张图片到磁盘,并最后触发当前关卡下载以最大化内存缓存命中率。
-  void _preloadNextImages() {
+  void _preloadNextImages() async {
     // 预加载数量 (包括当前关卡在内,共 20 个)
     const int totalPreloadCount = 20;
 
@@ -296,18 +291,18 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
 
     // 5. 循环触发 ItemLoader 加载
     int preloadCount = 0;
-    for (final itemToLoad in itemsToLoad) {
+    for (final item in itemsToLoad) {
       // 对远程图片进行预加载
-      // 调用 ItemLoader.load,它会使用 Download 单例进行下载和缓存
       // 我们不关心返回值或 Future,只是触发下载
-      if (itemToLoad is RemoteItem) {
+      if (item is RemoteItem) {
         try {
-          // 触发下载。对于非当前关卡,下载器会完成下载并写入磁盘,然后可能释放内存。
-          // 对于当前关卡 (最后一个被调用的),它留在内存中的可能性最大。
-          ItemLoader.load(itemToLoad);
+          // 使用静态 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: ${itemToLoad.id}, error: $e');
+          _log.warning('Failed to load item for preloading: ${item.id}, error: $e');
         }
       }
     }
@@ -381,9 +376,9 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
           IconButton(
             onPressed: () {
               audio.playSfx(SfxType.click);
+              // AppLovinMAX.showMediationDebugger();
               // Navigator.push(context, SettingsDialog.buildRoute());
               Navigator.push(context, SettingScreen.buildRoute());
-              // AppLovinMAX.showMediationDebugger();
             },
             icon: const Icon(Icons.settings, color: Colors.black87),
           ),
@@ -427,20 +422,10 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
           ),
           SafeArea(
             child: SizedBox(
-              // 始终预留一个固定的高度,防止布局跳变
+              // 始终预留高度,防止 Banner 出现时下方 UI 整体上跳(Layout Jitter)
               height: context.read<Device>().bannerHeight,
               width: double.infinity,
-              child: FutureBuilder<bool>(
-                future: _bannerReadyAndShouldShow(),
-                builder: (context, snapshot) {
-                  if (snapshot.hasData && snapshot.data == true) {
-                    return adBanner;
-                  }
-                  return Container(
-                    // color: Colors.grey.shade100,
-                  );
-                },
-              ),
+              child: isBannerVisible ? getBanner('home_bottom') : const SizedBox.shrink(), // 隐藏时完全不占位或保持留白
             ),
           ),
         ],
@@ -527,17 +512,36 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
     );
   }
 
+  // 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(
-    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)])),
+    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),
           ),
-        );
-      },
+        ],
+      ),
     ),
   );
 
@@ -618,12 +622,6 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
     applovinAdsController.initialize();
   }
 
-  /// gallery页面加载的时候,可能广告模块还没有初始化完毕
-  Future<bool> _bannerReadyAndShouldShow() async {
-    bool ready = await adSDKReady();
-    return ready && shouldShowBannerAd(data.currentLevel);
-  }
-
   /////////////////////////// FCM ///////////////////////////
   // 消息推送许可弹框
   initFCM() async {

+ 1 - 1
lib/main.dart

@@ -122,7 +122,7 @@ void main() async {
   await Persistence().initialize();
 
   // 远程参数初始化
-  await RemoteConfig().initialize();
+  RemoteConfig().initialize();
 
   // 记录程序运行时间
   Persistence().lastRunTime = DateTime.now();

+ 2 - 2
lib/models/api_helper.dart

@@ -18,8 +18,8 @@ class ApiHelper {
   // static String get apiHost => Config.isDebug ? localAVDHost : productionHost;
   // static String get resHost => Config.isDebug ? localAVDHost : cdnHost;
 
-  static String thumbUri(String id) => 'http://$resHost/res/jigstack/thumb/320/$id.jpg';
-  static String imageUri(String id) => 'http://$resHost/res/jigstack/coded/org/$id.jpg';
+  static String thumbUri(String id) => 'http://$resHost/res/jigstack/thumb/320/$id.jpeg';
+  static String imageUri(String id, String quality) => 'http://$resHost/res/jigstack/coded/$quality/$id.jpeg';
 
   static String get dailyUri => 'https://$apiHost/napi/jigstack/mobi/list/daily';
   static String get latestUri => 'https://$apiHost/napi/jigstack/mobi/list/latest';

+ 1 - 0
lib/models/cached_request.dart

@@ -4,6 +4,7 @@ import 'dart:convert';
 import 'package:flutter/material.dart'; // 引入 Flutter 核心包
 import 'package:http/http.dart' as http;
 import 'package:puzzleweave/models/api_helper.dart';
+import 'package:puzzleweave/models/data.dart';
 import 'package:puzzleweave/utils/utils.dart';
 import 'package:logging/logging.dart';
 

+ 39 - 0
lib/models/data.dart

@@ -23,10 +23,32 @@ class Data {
     // _persistence.completedWorks = works;
     // _persistence.completedCollections = [];
 
+    // 1. 先初始化内置索引(独立、确定、不依赖缓存判断)
+    await _initBuiltinRegistry();
+
     completedWorks.value = _persistence.completedWorks;
     completedCollections.value = _persistence.completedCollections;
   }
 
+  /// 独立初始化内置图索引,确保无论网络/磁盘缓存如何,Asset 里的图都能被识别
+  Future<void> _initBuiltinRegistry() async {
+    try {
+      _log.info('Initializing BuiltinRegistry from Asset...');
+      // 直接读取资源文件,不走 CachedRequest 逻辑
+      final latestData = await loadJSONFromAsset('assets/builtin/latest.json');
+      if (latestData['data'] != null) {
+        BuiltinRegistry.init(latestData['data']);
+        _log.info('BuiltinRegistry initialized with ${latestData['total']} items.');
+      }
+
+      // 如果 collection.json 也有内置 ID 需要保护,也可以在这里 init
+      // final collectionData = await loadJSONFromAsset('assets/builtin/collection.json');
+      // BuiltinRegistry.init(collectionData['data']);
+    } catch (e) {
+      _log.severe('Failed to initialize BuiltinRegistry: $e');
+    }
+  }
+
   // 完成某个作品调用此接口记录到存储中
   // !!! 改造点:接受 ListItem 和可选的耗时
   void workDone(ListItem item, {Duration? timeSpent}) {
@@ -132,3 +154,20 @@ class Data {
     return _collection!;
   }
 }
+
+// 内置图索引
+class BuiltinRegistry {
+  static final Set<String> _builtinIds = {};
+
+  // 在 App 启动(或 CachedRequest 加载内置数据时)调用
+  static void init(List<dynamic> data) {
+    _builtinIds.clear();
+    for (var item in data) {
+      if (item['_id'] != null) {
+        _builtinIds.add(item['_id']);
+      }
+    }
+  }
+
+  static bool contains(String id) => _builtinIds.contains(id);
+}

+ 110 - 175
lib/models/download.dart

@@ -3,33 +3,30 @@ import 'dart:ui' as ui;
 
 import 'package:flutter/foundation.dart';
 import 'package:http/http.dart';
-import 'package:puzzleweave/config/device.dart';
+import 'package:logging/logging.dart';
+import 'package:puzzleweave/models/api_helper.dart';
+import 'package:puzzleweave/models/data.dart';
 import 'package:puzzleweave/models/items.dart';
 import 'package:puzzleweave/utils/utils.dart';
-import 'package:logging/logging.dart';
 
 final Logger _log = Logger('download.dart');
 
 /// 最多缓存/并发下载n个图到内存
 const maxCachedItems = 1;
 
-/// Sigeleton
+/// Singleton
 class Download {
   static final Download _instance = Download._internal();
 
-  factory Download() {
-    return _instance;
-  }
+  factory Download() => _instance;
   Download._internal();
 
   final Map<String, DownloadItem> _cache = {};
 
-  DownloadItem download(url, cachePath) {
-    // 移除同步 _clean() 调用,避免干扰正在启动的下载序列
-    // _clean();
+  DownloadItem download(String url, String cachePath) {
     if (_cache[url] != null) {
       _log.info('Cache hit for $url');
-      _cache[url]!.touch(); //update last use time.
+      _cache[url]!.touch();
       return _cache[url]!;
     } else {
       final item = DownloadItem(url, cachePath);
@@ -53,11 +50,7 @@ class Download {
   }
 
   _clean() {
-    // final list = _cache.values.toList();
-    // 1. 筛选出可以被清理的项:必须是已完成加载(已完成下载并写入磁盘)
-    final list = _cache.values
-        .where((item) => item.loadCompleter.isCompleted) // !!! 修正点 2: 仅清理已完成加载的项
-        .toList();
+    final list = _cache.values.where((item) => item.loadCompleter.isCompleted).toList();
     if (list.length <= maxCachedItems) return;
     _log.info('cleaning...');
 
@@ -66,7 +59,7 @@ class Download {
     // 3. 清理到只剩下 maxCachedItems 个
     while (list.length > maxCachedItems) {
       final item = list.removeAt(0);
-      _log.info('clean item: $item');
+      _log.info('Cleaning item from memory: $item');
       item.dispose();
       _cache.remove(item.url);
     }
@@ -82,165 +75,101 @@ class DownloadItem {
   final String url;
   final String cachePath;
   ValueNotifier<double> progress = ValueNotifier(0.0);
-  final Completer<ui.Image> loadCompleter = Completer();
+  final Completer<void> loadCompleter = Completer(); // 仅作为完成信号
   Client? client;
   StreamSubscription? subscription;
   int lastUsed;
-  int size = 0;
-  ui.Image? image;
-  Uint8List? data;
+  Uint8List? _data;
 
   DownloadItem(this.url, this.cachePath) : lastUsed = DateTime.now().millisecondsSinceEpoch {
     _log.info('New download item for: $url');
     _start();
   }
 
-  touch() {
-    lastUsed = DateTime.now().millisecondsSinceEpoch;
-  }
+  Uint8List? get data => _data;
+  set data(Uint8List? val) => _data = val;
+
+  touch() => lastUsed = DateTime.now().millisecondsSinceEpoch;
 
   _start() async {
     try {
-      final image = await _download();
-      loadCompleter.complete(image);
+      await _download();
+      if (!loadCompleter.isCompleted) loadCompleter.complete();
     } catch (err) {
-      loadCompleter.completeError(err);
+      if (!loadCompleter.isCompleted) loadCompleter.completeError(err);
     }
   }
 
-  Future<ui.Image> _download() async {
+  Future<void> _download() async {
     progress.value = 0;
-
     final file = await localFile(cachePath);
     _checkDispose();
 
-    final Uint8List data;
-    bool shouldSave = false;
-
-    //if (await file.exists()) {
+    // 核心改造:如果文件存在,只报完成,不读数据 (Lazy Load)
     if (await file.exists()) {
-      _log.info('Disk cache hit..');
-      data = await file.readAsBytes();
-      _checkDispose();
-      progress.value = 1;
-    } else {
-      final List<int> bytes = [];
-
-      client = Client();
-
-      final uri = Uri.parse(url);
-      final request = Request('GET', uri);
-
-      final response = await client!.send(request);
-      _checkDispose();
-
-      if (response.statusCode != 200) {
-        throw Exception('Download error, stauts:${response.statusCode}, url=$uri');
-      }
-
-      if (response.contentLength == null) {
-        throw Exception('Download error, no length, url=$uri');
-      }
-
-      final length = response.contentLength!;
-
-      _log.info('message: contentLength=$length');
-
-      final streamCompleter = Completer();
-
-      subscription = response.stream.listen(
-        (value) {
-          try {
-            // 有可能内存溢出, 先try/catch包一下
-            bytes.addAll(value);
-            progress.value = bytes.length / length;
-            _log.info('message: progress=${progress.value}');
-          } catch (e) {
-            _log.warning("Out of memory: $e");
-            // FirebaseCrashlytics.instance.log("OOM from download url: $uri, error: $e");
-            streamCompleter.completeError(e);
-          }
-        },
-        onDone: () {
-          //_log.info('onDone..');
-          streamCompleter.complete();
-        },
-        onError: (e) {
-          _log.info('onError: $e');
-          streamCompleter.completeError(e);
-        },
-        cancelOnError: true,
-      );
-
-      await streamCompleter.future;
-      //await response.stream.first;
-      _log.info('xxxxxxxxxxxxxxxxxxxx stream complete');
-
-      _checkDispose();
-      _log.info('message: download succeed, progress=$progress, length=${bytes.length}');
-
-      bytes.removeRange(0, 24);
-      data = Uint8List.fromList(bytes);
-
-      shouldSave = true;
-      _checkDispose();
+      _log.info('Disk cache hit (Metadata only) for $cachePath');
+      progress.value = 1.0;
+      return;
     }
 
-    _log.info('message: realbytes: ${data.length}');
-
-    int size = Device.physicalSize.width.toInt();
-
-    final ui.Codec codec = await ui.instantiateImageCodec(data, targetHeight: size, targetWidth: size);
-    final ui.FrameInfo frameInfo = await codec.getNextFrame();
-    final image = frameInfo.image;
-    this.image = image;
+    // 网络下载逻辑
+    final List<int> bytes = [];
+    client = Client();
+    final response = await client!.send(Request('GET', Uri.parse(url)));
+    _checkDispose();
 
-    this.data = data;
-    size = data.length;
+    if (response.statusCode != 200) throw Exception('Status:${response.statusCode}');
+    final length = response.contentLength ?? 0;
+
+    final streamCompleter = Completer();
+    subscription = response.stream.listen(
+      (value) {
+        bytes.addAll(value);
+        if (length > 0) progress.value = bytes.length / length;
+      },
+      onDone: () => streamCompleter.complete(),
+      onError: (e) => streamCompleter.completeError(e),
+      cancelOnError: true,
+    );
+
+    await streamCompleter.future;
+    _checkDispose();
 
-    if (shouldSave) {
-      await saveBytes(cachePath, data);
+    // 剔除 24 字节干扰码
+    if (bytes.length > 24) {
+      bytes.removeRange(0, 24);
     }
 
-    _checkDispose();
+    _data = Uint8List.fromList(bytes);
+    await saveBytes(cachePath, _data!); // 写入磁盘
+    _log.info('Download and save complete for $url');
 
-    _log.info('image: ${image.width}x${image.height}');
     client?.close();
-
-    return image;
   }
 
-  Future<ui.Image> getImageBySize(int dim, {allowUpscaling = false}) async {
-    await loadCompleter.future;
-    final ui.Codec codec = await ui.instantiateImageCodec(data!, targetHeight: dim, targetWidth: dim, allowUpscaling: allowUpscaling);
-    final ui.FrameInfo frameInfo = await codec.getNextFrame();
-    return frameInfo.image;
+  /// 供 Loader 真正需要数据时调用
+  Future<Uint8List> ensureDataLoaded() async {
+    if (_data != null) return _data!;
+    _log.info('Performing late read from disk: $cachePath');
+    final file = await localFile(cachePath);
+    _data = await file.readAsBytes();
+    return _data!;
   }
 
   bool _isDisposed = false;
-
   _checkDispose() {
-    _log.info('$this,checkDispose: $_isDisposed');
-    if (_isDisposed) throw Exception('Request disposed');
+    if (_isDisposed) throw Exception('Disposed');
   }
 
-  dispose() async {
-    _log.info('Disposing $this, client:$client');
+  dispose() {
     _isDisposed = true;
-    // do clean.
-    try {
-      subscription?.cancel();
-      client?.close();
-      image?.dispose();
-    } catch (error) {
-      _log.info('xxxxxxxxxxxx $error');
-    }
+    _data = null; // 释放内存
+    subscription?.cancel();
+    client?.close();
   }
 
   @override
-  String toString() {
-    return '[$cachePath]';
-  }
+  String toString() => '[$cachePath]';
 }
 
 abstract class ItemLoader {
@@ -250,58 +179,53 @@ abstract class ItemLoader {
 
   ItemLoader();
 
-  Future<ui.Image> getImageBySize(int width, int height, {allowUpscaling = true}) async {
+  // 辅助方法:确保数据就绪后再解码
+  Future<Uint8List> _prepareData() async {
     await completer.future;
-    final ui.Codec codec = await ui.instantiateImageCodec(data!, targetHeight: height, targetWidth: width, allowUpscaling: allowUpscaling);
-    final ui.FrameInfo frameInfo = await codec.getNextFrame();
-    return frameInfo.image;
+    if (data == null) throw 'Data missing after completion';
+    return data!;
+  }
+
+  Future<ui.Image> getImageBySize(int width, int height, {allowUpscaling = true}) async {
+    final bytes = await _prepareData();
+    final codec = await ui.instantiateImageCodec(bytes, targetHeight: height, targetWidth: width, allowUpscaling: allowUpscaling);
+    return (await codec.getNextFrame()).image;
   }
 
   Future<ui.Image> getImage() async {
-    await completer.future;
-    final ui.Codec codec = await ui.instantiateImageCodec(data!);
-    final ui.FrameInfo frameInfo = await codec.getNextFrame();
-    return frameInfo.image;
+    final bytes = await _prepareData();
+    final codec = await ui.instantiateImageCodec(bytes);
+    return (await codec.getNextFrame()).image;
+  }
+
+  /// 专门用于预加载的静态方法,不创建 Loader 实例
+  static void preload(ListItem item, String quality) {
+    if (BuiltinRegistry.contains(item.id)) {
+      _log.info('Preload: Skipping builtin ${item.id}');
+      return;
+    }
+    if (item is RemoteItem) {
+      // 触发 Download 但不等待读入内存
+      Download().download(ApiHelper.imageUri(item.id, quality), item.cachePath);
+    }
   }
 
-  factory ItemLoader.load(ListItem item) {
+  factory ItemLoader.load(ListItem item, String quality) {
+    if (BuiltinRegistry.contains(item.id)) {
+      _log.info('Built-in hit: ${item.id}, skip network.');
+      return AssetItemLoader('assets/builtin/${item.id}.jpeg');
+    }
     switch (item.runtimeType) {
       case RemoteItem:
-        return RemoteItemLoader((item as RemoteItem).image, item.cachePath);
+        return RemoteItemLoader(ApiHelper.imageUri(item.id, quality), item.cachePath);
       case AssetItem:
         return AssetItemLoader((item as AssetItem).image);
       default:
-        throw 'Can\'t create ${item.runtimeType}';
+        throw 'Unknown item type';
     }
   }
 }
 
-class LocalItemLoader extends ItemLoader {
-  final String path;
-  Uint8List? _data;
-
-  @override
-  ValueNotifier<double> progress = ValueNotifier(0);
-
-  LocalItemLoader(this.path) {
-    _load();
-  }
-
-  _load() async {
-    try {
-      final file = await localFile(path);
-      _data = await file.readAsBytes();
-      completer.complete(data);
-      progress.value = 1.0;
-    } catch (error) {
-      completer.completeError(error);
-    }
-  }
-
-  @override
-  Uint8List? get data => _data;
-}
-
 class AssetItemLoader extends ItemLoader {
   final String path;
   Uint8List? _data;
@@ -316,10 +240,10 @@ class AssetItemLoader extends ItemLoader {
   _load() async {
     try {
       _data = await loadFileDataFromAsset(path);
-      completer.complete(data);
+      completer.complete();
       progress.value = 1.0;
-    } catch (error) {
-      completer.completeError(error);
+    } catch (e) {
+      completer.completeError(e);
     }
   }
 
@@ -340,6 +264,7 @@ class RemoteItemLoader extends ItemLoader {
   _load() async {
     try {
       await downloadItem.loadCompleter.future;
+      // 在这里不读 data,getImage 时才读
       completer.complete();
     } catch (err) {
       completer.completeError(err);
@@ -347,7 +272,17 @@ class RemoteItemLoader extends ItemLoader {
   }
 
   @override
-  Uint8List? get data => downloadItem.data;
+  Uint8List? get data {
+    // 同步获取(如果已读入内存),如果没读,需通过 getImage 异步触发
+    return downloadItem.data;
+  }
+
+  @override
+  Future<Uint8List> _prepareData() async {
+    await completer.future;
+    // 关键点:如果内存里没数据(预加载命中的缓存),在此处执行补读
+    return await downloadItem.ensureDataLoaded();
+  }
 
   @override
   ValueNotifier<double> get progress => downloadItem.progress;

+ 8 - 24
lib/play/board_play.dart

@@ -163,7 +163,9 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
   initState() {
     super.initState();
 
-    itemLoader = ItemLoader.load(widget.item);
+    final Device device = context.read<Device>();
+
+    itemLoader = ItemLoader.load(widget.item, device.suggestedQuality);
     _onProgressUpdate();
     itemLoader.progress.addListener(_onProgressUpdate);
     timer = Timer(const Duration(seconds: 5), () {
@@ -189,8 +191,6 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
       }
     });
 
-    Device device = context.read<Device>();
-
     // audio = context.read<AudioController>();
     audio = context.read<JcAudioController>();
     data = context.read<Data>();
@@ -651,7 +651,7 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
       _isLoading = true;
     });
 
-    final dpr = device.devicePixelRatio;
+    final dpr = device.effectivePixelRatio;
     final targetRect = device.targetRect;
     final bestImageSize = device.bestImageSize;
 
@@ -844,12 +844,6 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
     saveProgress();
   }
 
-  /// gallery页面加载的时候,可能广告模块还没有初始化完毕
-  Future<bool> _bannerReadyAndShouldShow() async {
-    bool ready = await adSDKReady();
-    return ready && shouldShowBannerAd(data.currentLevel);
-  }
-
   void _onWillPop(bool didPop, dynamic result) async {
     _log.info('board play will pop, dipPop=$didPop, result=$result');
     if (didPop) {
@@ -883,20 +877,10 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
               right: 0,
               child: SafeArea(
                 child: SizedBox(
-                  // 始终预留一个固定的高度,防止布局跳变
+                  // 始终预留高度,防止 Banner 出现时下方 UI 整体上跳(Layout Jitter)
                   height: context.read<Device>().bannerHeight,
                   width: double.infinity,
-                  child: FutureBuilder<bool>(
-                    future: _bannerReadyAndShouldShow(),
-                    builder: (context, snapshot) {
-                      if (snapshot.hasData && snapshot.data == true) {
-                        return adBanner;
-                      }
-                      return Container(
-                        // color: Colors.grey.shade100,
-                      );
-                    },
-                  ),
+                  child: isBannerVisible ? getBanner('playBottom') : const SizedBox.shrink(), // 隐藏时完全不占位或保持留白
                 ),
               ),
             ),
@@ -1089,8 +1073,8 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
               width: double.infinity, // 图片宽=容器宽
               height: double.infinity, // 图片高=容器高
               fit: BoxFit.cover, // 图片填充容器(不拉伸,超出部分裁剪)
-              cacheWidth: (context.watch<Device>().devicePixelRatio * bannerWidth).toInt(),
-              cacheHeight: (context.watch<Device>().devicePixelRatio * bannerHeight).toInt(),
+              cacheWidth: (context.watch<Device>().realPixelRatio * bannerWidth).toInt(),
+              cacheHeight: (context.watch<Device>().realPixelRatio * bannerHeight).toInt(),
             ),
             Center(
               child: Padding(

+ 18 - 4
lib/remote_config/remote_config.dart

@@ -19,7 +19,22 @@ class RemoteConfig {
   final _remoteConfig = FirebaseRemoteConfig.instance;
 
   Future<void> initialize() async {
-    await _remoteConfig.setConfigSettings(RemoteConfigSettings(fetchTimeout: const Duration(minutes: 1), minimumFetchInterval: const Duration(hours: 1)));
+    try {
+      // 异步执行,不阻塞 initialize 方法的返回
+      _realInit();
+    } catch (e) {
+      _log.warning("RemoteConfig setup triggered error: $e");
+    }
+  }
+
+  // 真正的初始化逻辑
+  Future<void> _realInit() async {
+    await _remoteConfig.setConfigSettings(
+      RemoteConfigSettings(
+        fetchTimeout: const Duration(seconds: 10), // 建议缩短超时时间,1分钟太久了
+        minimumFetchInterval: const Duration(hours: 1),
+      ),
+    );
 
     ///参数默认值
     await _remoteConfig.setDefaults(const {
@@ -33,11 +48,10 @@ class RemoteConfig {
 
     _remoteConfig.onConfigUpdated.listen((event) async {
       await _remoteConfig.activate();
-      _log.info("remoteConfig update: ${_remoteConfig.getAll().toString()}");
-      // Use the new config values here.
+      _log.info("remoteConfig update triggered");
     });
 
-    _fetchAndActivate();
+    await _fetchAndActivate();
   }
 
   _fetchAndActivate() async {

+ 3 - 3
pubspec.lock

@@ -457,11 +457,11 @@ packages:
     dependency: "direct main"
     description:
       path: "."
-      ref: master
-      resolved-ref: "3412ec4bbf2231cd7e09cdeadf9fed717f951142"
+      ref: "v1.0.3"
+      resolved-ref: b6ade748deedaf84371776e075c15af3f58cc6f5
       url: "git@git.jccytech.cn:guoziyi/jc_audio_player.git"
     source: git
-    version: "1.0.1"
+    version: "1.0.3"
   json_annotation:
     dependency: transitive
     description:

+ 7 - 3
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.7+7
+version: 1.0.8+8
 
 environment:
   sdk: ^3.8.1
@@ -63,16 +63,20 @@ dependencies:
   url_launcher: ^6.3.2
   app_tracking_transparency: ^2.0.6+1
   jc_audio_player:
-    version: ^1.0.0
+    version: ^1.0.3
     git:
       url: git@git.jccytech.cn:guoziyi/jc_audio_player.git
-      ref: master
+      ref: v1.0.3 # 建议锁定到 tag,更安全
   launch_review_latest: ^1.0.0
   flutter_launcher_icons: ^0.14.4
   flutter_native_splash: ^2.4.7
   flutter_svg: ^2.2.3
   adjust_sdk: ^5.0.1
 
+# dependency_overrides:
+#   jc_audio_player:
+#     path: ../jc_audio_player # 插件的本地路径
+
 dev_dependencies:
   flutter_test:
     sdk: flutter