@_nazmiforreal/flutter-ota 0.1.26 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/bin/flutter-ota-linux-x64 +0 -0
  2. package/bin/flutter-ota.js +12 -11
  3. package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +46 -13
  4. package/dart-src/packages/cli-tools/lib/src/backend.dart +6 -6
  5. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +2 -0
  6. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +7 -7
  7. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +4 -1
  8. package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +1 -1
  9. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +1 -1
  10. package/dart-src/packages/cli-tools/lib/src/config.dart +5 -5
  11. package/dart-src/packages/cli-tools/lib/src/operations.dart +2 -2
  12. package/dart-src/packages/cli-tools/pubspec.yaml +8 -21
  13. package/dart-src/packages/cli-tools/test/backends_test.dart +3 -3
  14. package/dart-src/packages/cli-tools/test/cli_test.dart +1 -1
  15. package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +2 -2
  16. package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +2 -2
  17. package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +4 -1
  18. package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +2 -2
  19. package/dart-src/packages/cli-tools/test/pocketbase_smoke_test.dart +1 -1
  20. package/dart-src/root/README.md +394 -0
  21. package/dart-src/root/analysis_options.yaml +21 -0
  22. package/dart-src/root/android/build.gradle +116 -0
  23. package/dart-src/root/android/consumer-rules.pro +23 -0
  24. package/dart-src/root/android/settings.gradle +1 -0
  25. package/dart-src/root/android/src/main/AndroidManifest.xml +21 -0
  26. package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/ApplyResult.kt +52 -0
  27. package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/BlacklistStore.kt +154 -0
  28. package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/BootDiagnosticStore.kt +150 -0
  29. package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/CrashGuard.kt +271 -0
  30. package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/FlutterPatcherApplication.kt +201 -0
  31. package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/FlutterPatcherAutoInitProvider.kt +93 -0
  32. package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/FlutterPatcherPlugin.kt +245 -0
  33. package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/LoaderHook.kt +298 -0
  34. package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatchManager.kt +1579 -0
  35. package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatcherConfig.kt +173 -0
  36. package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/SignatureVerifier.kt +177 -0
  37. package/dart-src/root/android/src/test/kotlin/com/flutter_patcher/flutter_patcher/CrashGuardTest.kt +61 -0
  38. package/dart-src/root/android/src/test/kotlin/com/flutter_patcher/flutter_patcher/LoaderHookTest.kt +25 -0
  39. package/dart-src/root/android/src/test/kotlin/com/flutter_patcher/flutter_patcher/PatchManagerTest.kt +401 -0
  40. package/dart-src/root/android/src/test/kotlin/com/flutter_patcher/flutter_patcher/PatcherConfigTest.kt +30 -0
  41. package/dart-src/root/bin/pack.dart +182 -0
  42. package/dart-src/root/lib/flutter_ota_kit.dart +1170 -0
  43. package/dart-src/root/lib/src/app_version_resolver.dart +36 -0
  44. package/dart-src/root/lib/src/aws_update_source.dart +97 -0
  45. package/dart-src/root/lib/src/blacklist.dart +54 -0
  46. package/dart-src/root/lib/src/boot_diagnostic.dart +138 -0
  47. package/dart-src/root/lib/src/cloudflare_update_source.dart +97 -0
  48. package/dart-src/root/lib/src/flutter_ota_app.dart +69 -0
  49. package/dart-src/root/lib/src/io.dart +48 -0
  50. package/dart-src/root/lib/src/io_stub.dart +19 -0
  51. package/dart-src/root/lib/src/ota_progress_overlay.dart +987 -0
  52. package/dart-src/root/lib/src/pack/pack.dart +434 -0
  53. package/dart-src/root/lib/src/patch_info.dart +12 -0
  54. package/dart-src/root/lib/src/patcher_channel.dart +160 -0
  55. package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_database.dart +1 -1
  56. package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_s3_client.dart +1 -1
  57. package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_storage.dart +1 -1
  58. package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_storage_profile.dart +1 -1
  59. package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/with_cloudfront_signed_url.dart +1 -1
  60. package/dart-src/root/lib/src/pkg/client/flutter_ota_kit_client.dart +1 -0
  61. package/dart-src/root/lib/src/pkg/client/src/server_update_source.dart +46 -0
  62. package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/cloudflare_worker_database.dart +1 -1
  63. package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_build_where.dart +1 -1
  64. package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_bundle_mapper.dart +3 -3
  65. package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_database.dart +1 -1
  66. package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_database_plugin.dart +2 -2
  67. package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/r2_s3_client.dart +1 -1
  68. package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/r2_storage.dart +1 -1
  69. package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/r2_storage_profile.dart +1 -1
  70. package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/status.dart +1 -1
  71. package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/flutter_ota_kit_plugin_core.dart +1 -1
  72. package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/create_blob_database_plugin.dart +1 -1
  73. package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/filter_compatible_app_versions.dart +1 -1
  74. package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/get_update_info.dart +1 -1
  75. package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/request_update_bundle_state.dart +1 -1
  76. package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/resolve_update_info_from_bundles.dart +1 -1
  77. package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/types.dart +2 -2
  78. package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_bundle_mapper.dart +2 -2
  79. package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_database.dart +26 -15
  80. package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_storage.dart +1 -1
  81. package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_bundle_mapper.dart +2 -2
  82. package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_database.dart +2 -2
  83. package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_get_update_info.dart +2 -2
  84. package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_storage.dart +1 -1
  85. package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_types.dart +1 -1
  86. package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_bundle_mapper.dart +2 -2
  87. package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_database.dart +2 -2
  88. package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_edge_function_database.dart +1 -1
  89. package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_edge_function_storage.dart +1 -1
  90. package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_storage.dart +1 -1
  91. package/dart-src/root/lib/src/pocketbase_update_source.dart +107 -0
  92. package/dart-src/root/lib/src/postgres_update_source.dart +95 -0
  93. package/dart-src/root/lib/src/shared_update_check.dart +131 -0
  94. package/dart-src/root/lib/src/supabase_update_source.dart +93 -0
  95. package/dart-src/root/pubspec.yaml +54 -0
  96. package/package.json +1 -1
  97. package/scripts/postinstall.js +17 -13
  98. package/scripts/vendor.sh +22 -7
  99. package/dart-src/packages/core/CHANGELOG.md +0 -21
  100. package/dart-src/packages/core/README.md +0 -23
  101. package/dart-src/packages/core/example/lib/main.dart +0 -69
  102. package/dart-src/packages/core/pubspec.yaml +0 -13
  103. package/dart-src/packages/core/test/rollout_test.dart +0 -135
  104. package/dart-src/packages/core/test/semver_test.dart +0 -55
  105. package/dart-src/packages/core/test/semver_vectors.json +0 -1
  106. package/dart-src/packages/core/test/types_test.dart +0 -131
  107. package/dart-src/packages/core/test/uuid_test.dart +0 -40
  108. package/dart-src/packages/core/tool/dbg.dart +0 -7
  109. package/dart-src/plugins/aws/.gitkeep +0 -0
  110. package/dart-src/plugins/aws/CHANGELOG.md +0 -23
  111. package/dart-src/plugins/aws/LICENSE +0 -21
  112. package/dart-src/plugins/aws/README.md +0 -28
  113. package/dart-src/plugins/aws/example/lib/main.dart +0 -60
  114. package/dart-src/plugins/aws/pubspec.yaml +0 -30
  115. package/dart-src/plugins/aws/test/aws_database_test.dart +0 -161
  116. package/dart-src/plugins/aws/test/aws_storage_test.dart +0 -73
  117. package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +0 -78
  118. package/dart-src/plugins/cloudflare/.gitkeep +0 -0
  119. package/dart-src/plugins/cloudflare/CHANGELOG.md +0 -23
  120. package/dart-src/plugins/cloudflare/LICENSE +0 -21
  121. package/dart-src/plugins/cloudflare/README.md +0 -25
  122. package/dart-src/plugins/cloudflare/example/lib/main.dart +0 -62
  123. package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +0 -16
  124. package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +0 -8
  125. package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +0 -38
  126. package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +0 -11
  127. package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +0 -24
  128. package/dart-src/plugins/cloudflare/pubspec.yaml +0 -28
  129. package/dart-src/plugins/cloudflare/sql/bundles.sql +0 -42
  130. package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +0 -126
  131. package/dart-src/plugins/cloudflare/test/d1_bundle_mapper_test.dart +0 -106
  132. package/dart-src/plugins/cloudflare/test/d1_database_test.dart +0 -200
  133. package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +0 -261
  134. package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +0 -78
  135. package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +0 -74
  136. package/dart-src/plugins/plugin-core/CHANGELOG.md +0 -20
  137. package/dart-src/plugins/plugin-core/LICENSE +0 -21
  138. package/dart-src/plugins/plugin-core/README.md +0 -21
  139. package/dart-src/plugins/plugin-core/example/lib/main.dart +0 -61
  140. package/dart-src/plugins/plugin-core/pubspec.yaml +0 -23
  141. package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +0 -170
  142. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_store_test.dart +0 -74
  143. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +0 -345
  144. package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +0 -843
  145. package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +0 -298
  146. package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +0 -195
  147. package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +0 -56
  148. package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +0 -1589
  149. package/dart-src/plugins/plugin-core/test/helpers_test.dart +0 -283
  150. package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +0 -202
  151. package/dart-src/plugins/plugin-core/test/request_update_bundle_state_test.dart +0 -86
  152. package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +0 -88
  153. package/dart-src/plugins/pocketbase/CHANGELOG.md +0 -23
  154. package/dart-src/plugins/pocketbase/LICENSE +0 -21
  155. package/dart-src/plugins/pocketbase/README.md +0 -54
  156. package/dart-src/plugins/pocketbase/example/lib/main.dart +0 -62
  157. package/dart-src/plugins/pocketbase/pubspec.yaml +0 -26
  158. package/dart-src/plugins/pocketbase/test/pb_uri_test.dart +0 -12
  159. package/dart-src/plugins/postgres/.gitkeep +0 -0
  160. package/dart-src/plugins/postgres/CHANGELOG.md +0 -23
  161. package/dart-src/plugins/postgres/LICENSE +0 -21
  162. package/dart-src/plugins/postgres/README.md +0 -29
  163. package/dart-src/plugins/postgres/example/lib/main.dart +0 -67
  164. package/dart-src/plugins/postgres/pubspec.yaml +0 -26
  165. package/dart-src/plugins/postgres/sql/bundles.sql +0 -45
  166. package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +0 -21
  167. package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +0 -171
  168. package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +0 -126
  169. package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +0 -125
  170. package/dart-src/plugins/postgres/sql/hash_user_id.sql +0 -30
  171. package/dart-src/plugins/postgres/sql/is_device_eligible.sql +0 -237
  172. package/dart-src/plugins/postgres/sql/prepareSql.ts +0 -11
  173. package/dart-src/plugins/postgres/test/mock_postgres_client.dart +0 -320
  174. package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +0 -139
  175. package/dart-src/plugins/postgres/test/postgres_database_test.dart +0 -210
  176. package/dart-src/plugins/postgres/test/postgres_storage_runtime_test.dart +0 -60
  177. package/dart-src/plugins/supabase/CHANGELOG.md +0 -24
  178. package/dart-src/plugins/supabase/LICENSE +0 -21
  179. package/dart-src/plugins/supabase/README.md +0 -53
  180. package/dart-src/plugins/supabase/example/lib/main.dart +0 -63
  181. package/dart-src/plugins/supabase/pubspec.yaml +0 -27
  182. package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +0 -44
  183. package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +0 -968
  184. package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +0 -232
  185. package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +0 -137
  186. package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +0 -206
  187. package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +0 -186
  188. package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +0 -505
  189. package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +0 -50
  190. package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +0 -20
  191. package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +0 -75
  192. package/dart-src/plugins/supabase/test/mock_supabase_client.dart +0 -654
  193. package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +0 -115
  194. package/dart-src/plugins/supabase/test/supabase_database_test.dart +0 -198
  195. package/dart-src/plugins/supabase/test/supabase_storage_test.dart +0 -168
  196. /package/dart-src/{packages/core → root}/LICENSE +0 -0
  197. /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/flutter_ota_kit_aws.dart +0 -0
  198. /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_cloudfront_client.dart +0 -0
  199. /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_cloudfront_signer.dart +0 -0
  200. /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_config.dart +0 -0
  201. /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_ssm_client.dart +0 -0
  202. /package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/flutter_ota_kit_cloudflare.dart +0 -0
  203. /package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_client.dart +0 -0
  204. /package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_config.dart +0 -0
  205. /package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/r2_config.dart +0 -0
  206. /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/flutter_ota_kit_core.dart +0 -0
  207. /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/app_update_info.dart +0 -0
  208. /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/bundle.dart +0 -0
  209. /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/bundle_artifacts.dart +0 -0
  210. /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/bundle_patch_artifact.dart +0 -0
  211. /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/changed_asset.dart +0 -0
  212. /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/get_bundles_args.dart +0 -0
  213. /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/metadata.dart +0 -0
  214. /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/patch_info.dart +0 -0
  215. /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/platform.dart +0 -0
  216. /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/rollout.dart +0 -0
  217. /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/semver.dart +0 -0
  218. /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/semver_range.dart +0 -0
  219. /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/semver_version.dart +0 -0
  220. /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/strategy.dart +0 -0
  221. /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/update_bundle_params.dart +0 -0
  222. /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/uuid.dart +0 -0
  223. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/asset_storage_layout.dart +0 -0
  224. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/bundle_helpers.dart +0 -0
  225. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/bundle_storage_layout.dart +0 -0
  226. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/bundle_unit_of_work.dart +0 -0
  227. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/bundle_unit_of_work_store.dart +0 -0
  228. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/calculate_pagination.dart +0 -0
  229. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/compression_format.dart +0 -0
  230. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/content_addressed_assets.dart +0 -0
  231. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/create_database_plugin.dart +0 -0
  232. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/create_storage_key_builder.dart +0 -0
  233. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/create_storage_plugin.dart +0 -0
  234. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/generate_min_bundle_id.dart +0 -0
  235. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/is_object.dart +0 -0
  236. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/legacy_asset_storage_layout.dart +0 -0
  237. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/paginate_bundles.dart +0 -0
  238. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/parse_storage_uri.dart +0 -0
  239. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/query_bundles.dart +0 -0
  240. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/storage_helpers.dart +0 -0
  241. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/storage_profile.dart +0 -0
  242. /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/uuidv7.dart +0 -0
  243. /package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/flutter_ota_kit_pocketbase.dart +0 -0
  244. /package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_client.dart +0 -0
  245. /package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_config.dart +0 -0
  246. /package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/flutter_ota_kit_postgres.dart +0 -0
  247. /package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_client.dart +0 -0
  248. /package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_config.dart +0 -0
  249. /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/edge.dart +0 -0
  250. /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/flutter_ota_kit_supabase.dart +0 -0
  251. /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/error_message.dart +0 -0
  252. /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_client_adapter.dart +0 -0
  253. /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_client_http.dart +0 -0
  254. /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_config.dart +0 -0
  255. /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_signed_url_batcher.dart +0 -0
  256. /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/types.dart +0 -0
@@ -0,0 +1,987 @@
1
+ import 'dart:async';
2
+
3
+ import 'package:flutter/material.dart';
4
+ import 'package:flutter_ota_kit/src/pkg/core/flutter_ota_kit_core.dart'
5
+ show PatchApplyPhase, PatchApplyProgress;
6
+
7
+ /// Snapshot of the forced-update progress overlay's visual state.
8
+ class OtaOverlayState {
9
+ final PatchApplyPhase? phase;
10
+ final double? fraction;
11
+ final String? message;
12
+ final bool hasError;
13
+ final String? errorText;
14
+ final String? targetVersion;
15
+ final String? currentVersion;
16
+ final String? errorHint;
17
+ final String? bundleHash;
18
+ final String? gitCommit;
19
+ final String? commitMessage;
20
+ final String? channel;
21
+ final String? platform;
22
+ final int? bytesPerSec;
23
+
24
+ /// Bytes downloaded so far (downloading phase only).
25
+ final int bytesReceived;
26
+
27
+ /// Total payload size in bytes, or 0 when unknown.
28
+ final int totalBytes;
29
+
30
+ final List<LogLine> logs;
31
+ final bool canRetry;
32
+
33
+ /// Which step index is currently active (0-based). Steps that are done
34
+ /// get a check; the active step gets the braille spinner.
35
+ final int activeStep;
36
+
37
+ const OtaOverlayState({
38
+ this.phase,
39
+ this.fraction,
40
+ this.message,
41
+ this.hasError = false,
42
+ this.errorText,
43
+ this.targetVersion,
44
+ this.currentVersion,
45
+ this.errorHint,
46
+ this.bundleHash,
47
+ this.gitCommit,
48
+ this.commitMessage,
49
+ this.channel,
50
+ this.platform,
51
+ this.bytesPerSec,
52
+ this.bytesReceived = 0,
53
+ this.totalBytes = 0,
54
+ this.logs = const [],
55
+ this.canRetry = false,
56
+ this.activeStep = 0,
57
+ });
58
+
59
+ OtaOverlayState copyWith({
60
+ PatchApplyPhase? phase,
61
+ double? fraction,
62
+ String? message,
63
+ Object? hasError = _unset,
64
+ String? errorText,
65
+ String? targetVersion,
66
+ String? currentVersion,
67
+ String? errorHint,
68
+ String? bundleHash,
69
+ String? gitCommit,
70
+ String? channel,
71
+ String? platform,
72
+ int? bytesPerSec,
73
+ int? bytesReceived,
74
+ int? totalBytes,
75
+ List<LogLine>? logs,
76
+ bool? canRetry,
77
+ int? activeStep,
78
+ }) {
79
+ return OtaOverlayState(
80
+ phase: phase ?? this.phase,
81
+ fraction: fraction ?? this.fraction,
82
+ message: message ?? this.message,
83
+ hasError: identical(hasError, _unset) ? this.hasError : hasError as bool,
84
+ errorText: errorText ?? this.errorText,
85
+ targetVersion: targetVersion ?? this.targetVersion,
86
+ currentVersion: currentVersion ?? this.currentVersion,
87
+ errorHint: errorHint ?? this.errorHint,
88
+ bundleHash: bundleHash ?? this.bundleHash,
89
+ gitCommit: gitCommit ?? this.gitCommit,
90
+ commitMessage: commitMessage ?? this.commitMessage,
91
+ channel: channel ?? this.channel,
92
+ platform: platform ?? this.platform,
93
+ bytesPerSec: bytesPerSec ?? this.bytesPerSec,
94
+ bytesReceived: bytesReceived ?? this.bytesReceived,
95
+ totalBytes: totalBytes ?? this.totalBytes,
96
+ logs: logs ?? this.logs,
97
+ canRetry: canRetry ?? this.canRetry,
98
+ activeStep: activeStep ?? this.activeStep,
99
+ );
100
+ }
101
+ }
102
+
103
+ /// A single terminal log line with a color tag and an optional left-column tag.
104
+ ///
105
+ /// [text] is the message. If it contains a leading token separated by 2+
106
+ /// spaces (e.g. `'verify passed ✓'`), the overlay renders the first token
107
+ /// in the accent color and the rest in the body color, preserving the
108
+ /// aligned key/value look. Plain lines render whole in [color].
109
+ class LogLine {
110
+ final String color;
111
+ final String text;
112
+
113
+ const LogLine(this.color, this.text);
114
+ }
115
+
116
+ const Object _unset = Object();
117
+
118
+ /// GitHub-dark inspired palette for the terminal overlay.
119
+ class _C {
120
+ static const bg = Color(0xFF0B0F14);
121
+ static const panel = Color(0xFF11161D);
122
+ static const inner = Color(0xFF0E141B);
123
+ static const head = Color(0xFF161C24);
124
+ static const border = Color(0xFF232A34);
125
+ static const sep = Color(0xFF1B222B);
126
+ static const logBg = Color(0xFF0A0E13);
127
+ static const green = Color(0xFF3FB950);
128
+ static const blue = Color(0xFF58A6FF);
129
+ static const cyan = Color(0xFF56D4DD);
130
+ static const yellow = Color(0xFFD29922);
131
+ static const red = Color(0xFFF85149);
132
+ static const text = Color(0xFFC9D1D9);
133
+ static const dim = Color(0xFF7D8794);
134
+ static const faint = Color(0xFF484F58);
135
+ }
136
+
137
+ const String _mono = 'monospace';
138
+
139
+ /// The 5 steps shown during a forced update.
140
+ const List<String> _stepLabels = [
141
+ 'Initialize',
142
+ 'Download bundle',
143
+ 'Verify hash + signature',
144
+ 'Install patch',
145
+ 'Finalize & restart',
146
+ ];
147
+
148
+ /// Named color -> swatch, used to resolve [LogLine.color].
149
+ const Map<String, Color> _logColors = {
150
+ 'green': _C.green,
151
+ 'red': _C.red,
152
+ 'yellow': _C.yellow,
153
+ 'gray': _C.dim,
154
+ 'white': _C.text,
155
+ 'cyan': _C.cyan,
156
+ 'success': _C.blue,
157
+ };
158
+
159
+ /// Full-screen, structured terminal-style forced-update overlay.
160
+ ///
161
+ /// Sections, top to bottom:
162
+ /// - Title bar (traffic lights + status pill)
163
+ /// - META (channel / version / bundle / size)
164
+ /// - STEPS (per-phase status list)
165
+ /// - PROGRESS (monospace bar + speed / eta / phase)
166
+ /// - LOG (scrolling colored feed)
167
+ /// - Footer (deploy message, or error hint + retry)
168
+ class OtaProgressOverlay extends StatefulWidget {
169
+ final ValueNotifier<OtaOverlayState> state;
170
+ final bool dismissible;
171
+
172
+ /// Called when the user taps the footer `[ retry ]` button in the error
173
+ /// state. When null, the button is shown only if [OtaOverlayState.canRetry].
174
+ final VoidCallback? onRetry;
175
+
176
+ const OtaProgressOverlay({
177
+ super.key,
178
+ required this.state,
179
+ this.dismissible = false,
180
+ this.onRetry,
181
+ });
182
+
183
+ @override
184
+ State<OtaProgressOverlay> createState() => _OtaProgressOverlayState();
185
+ }
186
+
187
+ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
188
+ int _spinnerIndex = 0;
189
+ Timer? _spinnerTimer;
190
+
191
+ static const _braille = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
192
+
193
+ @override
194
+ void initState() {
195
+ super.initState();
196
+ _spinnerTimer = Timer.periodic(const Duration(milliseconds: 80), (_) {
197
+ if (!mounted) return;
198
+ setState(() => _spinnerIndex = (_spinnerIndex + 1) % _braille.length);
199
+ });
200
+ }
201
+
202
+ @override
203
+ void dispose() {
204
+ _spinnerTimer?.cancel();
205
+ super.dispose();
206
+ }
207
+
208
+ String _pct(double? f) =>
209
+ f == null ? '--%' : '${(f.clamp(0.0, 1.0) * 100).toStringAsFixed(0)}%';
210
+
211
+ String _mb(int bytes) => '${(bytes / 1048576).toStringAsFixed(1)} MB';
212
+
213
+ @override
214
+ Widget build(BuildContext context) {
215
+ return ValueListenableBuilder<OtaOverlayState>(
216
+ valueListenable: widget.state,
217
+ builder: (context, s, _) {
218
+ return Material(
219
+ color: _C.bg,
220
+ child: SafeArea(
221
+ child: Padding(
222
+ padding: const EdgeInsets.all(16),
223
+ child: Container(
224
+ decoration: BoxDecoration(
225
+ color: _C.panel,
226
+ borderRadius: BorderRadius.circular(12),
227
+ border: Border.all(color: _C.border),
228
+ ),
229
+ child: Column(
230
+ crossAxisAlignment: CrossAxisAlignment.stretch,
231
+ children: [
232
+ _titleBar(s),
233
+ Expanded(
234
+ child: SingleChildScrollView(
235
+ padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
236
+ child: Column(
237
+ crossAxisAlignment: CrossAxisAlignment.stretch,
238
+ children: [
239
+ _sectionLabel('META'),
240
+ const SizedBox(height: 8),
241
+ _metaBlock(s),
242
+ const SizedBox(height: 18),
243
+ _sectionLabel('STEPS'),
244
+ const SizedBox(height: 8),
245
+ _stepsBlock(s),
246
+ const SizedBox(height: 18),
247
+ _sectionLabel('PROGRESS'),
248
+ const SizedBox(height: 8),
249
+ _progressBlock(s),
250
+ const SizedBox(height: 18),
251
+ _sectionLabel('LOG'),
252
+ const SizedBox(height: 8),
253
+ _logBlock(s),
254
+ ],
255
+ ),
256
+ ),
257
+ ),
258
+ _footer(s),
259
+ ],
260
+ ),
261
+ ),
262
+ ),
263
+ ),
264
+ );
265
+ },
266
+ );
267
+ }
268
+
269
+ // ── Title bar ─────────────────────────────────────────────────────────
270
+ Widget _titleBar(OtaOverlayState s) => Container(
271
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
272
+ decoration: const BoxDecoration(
273
+ color: _C.head,
274
+ borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
275
+ border: Border(bottom: BorderSide(color: _C.border)),
276
+ ),
277
+ child: Row(
278
+ children: [
279
+ _dot(_C.red),
280
+ const SizedBox(width: 6),
281
+ _dot(_C.yellow),
282
+ const SizedBox(width: 6),
283
+ _dot(_C.green),
284
+ const SizedBox(width: 12),
285
+ const Flexible(
286
+ child: Text(
287
+ 'flutter-ota · forced update',
288
+ overflow: TextOverflow.ellipsis,
289
+ style: TextStyle(
290
+ color: _C.text, fontFamily: _mono, fontSize: 12),
291
+ ),
292
+ ),
293
+ const SizedBox(width: 8),
294
+ _pill(
295
+ s.hasError ? 'FAILED' : 'RUNNING',
296
+ s.hasError ? _C.red : _C.green,
297
+ ),
298
+ ],
299
+ ),
300
+ );
301
+
302
+ // ── META ──────────────────────────────────────────────────────────────
303
+ Widget _metaBlock(OtaOverlayState s) {
304
+ final rows = <List<String>>[
305
+ if (s.channel != null) ['channel', s.channel!],
306
+ if (s.currentVersion != null && s.targetVersion != null)
307
+ ['version', '${s.currentVersion} → ${s.targetVersion}']
308
+ else if (s.targetVersion != null)
309
+ ['version', s.targetVersion!],
310
+ if (s.bundleHash != null) ['bundle', s.bundleHash!],
311
+ if (s.totalBytes > 0) ['size', _mb(s.totalBytes)],
312
+ ];
313
+ if (rows.isEmpty) rows.add(['status', 'preparing…']);
314
+ return _panelBox(
315
+ child: Column(
316
+ crossAxisAlignment: CrossAxisAlignment.start,
317
+ children: [
318
+ for (final r in rows)
319
+ Padding(
320
+ padding: const EdgeInsets.symmetric(vertical: 2),
321
+ child: RichText(
322
+ text: TextSpan(
323
+ style: const TextStyle(fontFamily: _mono, fontSize: 13),
324
+ children: [
325
+ TextSpan(
326
+ text: '${r[0].padRight(9)} ',
327
+ style: const TextStyle(color: _C.dim)),
328
+ TextSpan(
329
+ text: r[1],
330
+ style: TextStyle(
331
+ color: r[0] == 'version' ? _C.blue : _C.text)),
332
+ ],
333
+ ),
334
+ ),
335
+ ),
336
+ ],
337
+ ),
338
+ );
339
+ }
340
+
341
+ // ── STEPS ─────────────────────────────────────────────────────────────
342
+ Widget _stepsBlock(OtaOverlayState s) {
343
+ final spinner = _braille[_spinnerIndex];
344
+ return _panelBox(
345
+ child: Column(
346
+ crossAxisAlignment: CrossAxisAlignment.start,
347
+ children: List.generate(_stepLabels.length, (i) {
348
+ final done = i < s.activeStep;
349
+ final active = i == s.activeStep && !s.hasError;
350
+ final failed = i == s.activeStep && s.hasError;
351
+ final pending = i > s.activeStep;
352
+
353
+ final Color color = failed
354
+ ? _C.red
355
+ : done
356
+ ? _C.green
357
+ : active
358
+ ? _C.blue
359
+ : _C.faint;
360
+
361
+ final String glyph = failed
362
+ ? '✗'
363
+ : done
364
+ ? '✓'
365
+ : active
366
+ ? spinner
367
+ : '·';
368
+
369
+ final String status = failed
370
+ ? 'error'
371
+ : done
372
+ ? 'done'
373
+ : active
374
+ ? _pct(s.fraction)
375
+ : 'wait';
376
+
377
+ return Padding(
378
+ padding: const EdgeInsets.symmetric(vertical: 3),
379
+ child: Row(
380
+ children: [
381
+ SizedBox(
382
+ width: 18,
383
+ child: Text(glyph,
384
+ style: TextStyle(
385
+ color: color, fontFamily: _mono, fontSize: 14)),
386
+ ),
387
+ const SizedBox(width: 8),
388
+ Expanded(
389
+ child: Text(
390
+ _stepLabels[i],
391
+ style: TextStyle(
392
+ color: pending ? _C.faint : _C.text,
393
+ fontFamily: _mono,
394
+ fontSize: 13,
395
+ fontWeight: active ? FontWeight.w600 : FontWeight.w400,
396
+ ),
397
+ ),
398
+ ),
399
+ Text(
400
+ status,
401
+ style: TextStyle(
402
+ color: color,
403
+ fontFamily: _mono,
404
+ fontSize: 12,
405
+ fontFeatures: const [FontFeature.tabularFigures()],
406
+ ),
407
+ ),
408
+ ],
409
+ ),
410
+ );
411
+ }),
412
+ ),
413
+ );
414
+ }
415
+
416
+ // ── PROGRESS ──────────────────────────────────────────────────────────
417
+ Widget _progressBlock(OtaOverlayState s) {
418
+ const total = 34;
419
+ final frac = (s.fraction ?? 0).clamp(0.0, 1.0);
420
+ final filled = (frac * total).round();
421
+ final bar = '${'█' * filled}${'░' * (total - filled)}';
422
+
423
+ final speed = (s.bytesPerSec != null && s.bytesPerSec! > 0)
424
+ ? '${(s.bytesPerSec! / 1048576).toStringAsFixed(1)} MB/s'
425
+ : '—';
426
+ String eta = '—';
427
+ if (s.bytesPerSec != null &&
428
+ s.bytesPerSec! > 0 &&
429
+ s.totalBytes > s.bytesReceived) {
430
+ final secs = ((s.totalBytes - s.bytesReceived) / s.bytesPerSec!).ceil();
431
+ eta = '${secs}s';
432
+ }
433
+ final phase = s.hasError
434
+ ? 'halted'
435
+ : (s.phase?.name ?? 'preparing');
436
+
437
+ return _panelBox(
438
+ child: Column(
439
+ crossAxisAlignment: CrossAxisAlignment.start,
440
+ children: [
441
+ RichText(
442
+ text: TextSpan(
443
+ style: const TextStyle(fontFamily: _mono, fontSize: 13),
444
+ children: [
445
+ TextSpan(
446
+ text: bar,
447
+ style: TextStyle(color: s.hasError ? _C.red : _C.blue)),
448
+ TextSpan(
449
+ text: ' ${_pct(s.fraction)}',
450
+ style: const TextStyle(
451
+ color: _C.text, fontWeight: FontWeight.w700)),
452
+ ],
453
+ ),
454
+ ),
455
+ const SizedBox(height: 10),
456
+ Row(
457
+ children: [
458
+ _stat('speed', speed),
459
+ _stat('eta', eta),
460
+ _stat('phase', phase),
461
+ ],
462
+ ),
463
+ ],
464
+ ),
465
+ );
466
+ }
467
+
468
+ // ── LOG ───────────────────────────────────────────────────────────────
469
+ Widget _logBlock(OtaOverlayState s) {
470
+ return Container(
471
+ height: 132,
472
+ padding: const EdgeInsets.all(12),
473
+ decoration: BoxDecoration(
474
+ color: _C.logBg,
475
+ borderRadius: BorderRadius.circular(8),
476
+ border: Border.all(color: _C.sep),
477
+ ),
478
+ child: _TerminalLog(logs: s.logs),
479
+ );
480
+ }
481
+
482
+ // ── Footer ────────────────────────────────────────────────────────────
483
+ Widget _footer(OtaOverlayState s) {
484
+ final showRetry = s.hasError && (widget.onRetry != null || s.canRetry);
485
+ return Container(
486
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
487
+ decoration: const BoxDecoration(
488
+ color: _C.head,
489
+ borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)),
490
+ border: Border(top: BorderSide(color: _C.border)),
491
+ ),
492
+ child: s.hasError
493
+ ? Row(
494
+ children: [
495
+ Expanded(
496
+ child: Text(
497
+ s.errorHint ?? 'Close the app and reopen to retry.',
498
+ style: const TextStyle(
499
+ color: _C.dim, fontFamily: _mono, fontSize: 12),
500
+ ),
501
+ ),
502
+ if (showRetry)
503
+ GestureDetector(
504
+ onTap: widget.onRetry,
505
+ child: Container(
506
+ padding: const EdgeInsets.symmetric(
507
+ horizontal: 14, vertical: 7),
508
+ decoration: BoxDecoration(
509
+ borderRadius: BorderRadius.circular(7),
510
+ border: Border.all(color: _C.border),
511
+ ),
512
+ child: const Text('[ retry ]',
513
+ style: TextStyle(
514
+ color: _C.blue,
515
+ fontFamily: _mono,
516
+ fontSize: 13)),
517
+ ),
518
+ ),
519
+ ],
520
+ )
521
+ : Row(
522
+ children: [
523
+ const Text('›',
524
+ style: TextStyle(
525
+ color: _C.green, fontFamily: _mono, fontSize: 13)),
526
+ const SizedBox(width: 8),
527
+ Expanded(
528
+ child: Text(
529
+ (s.message != null && s.message!.isNotEmpty)
530
+ ? s.message!
531
+ : 'Applying update, please keep the app open…',
532
+ style: const TextStyle(
533
+ color: _C.dim, fontFamily: _mono, fontSize: 12),
534
+ ),
535
+ ),
536
+ ],
537
+ ),
538
+ );
539
+ }
540
+
541
+ // ── helpers ─────────────────────────────────────────────────────────
542
+ Widget _sectionLabel(String t) => Row(
543
+ children: [
544
+ Text(t,
545
+ style: const TextStyle(
546
+ color: _C.dim,
547
+ fontFamily: _mono,
548
+ fontSize: 11,
549
+ fontWeight: FontWeight.w700,
550
+ letterSpacing: 1.5,
551
+ )),
552
+ const SizedBox(width: 10),
553
+ const Expanded(child: Divider(color: _C.sep, height: 1)),
554
+ ],
555
+ );
556
+
557
+ Widget _panelBox({required Widget child}) => Container(
558
+ width: double.infinity,
559
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
560
+ decoration: BoxDecoration(
561
+ color: _C.inner,
562
+ borderRadius: BorderRadius.circular(8),
563
+ border: Border.all(color: _C.sep),
564
+ ),
565
+ child: child,
566
+ );
567
+
568
+ Widget _stat(String k, String v) => Expanded(
569
+ child: Column(
570
+ crossAxisAlignment: CrossAxisAlignment.start,
571
+ children: [
572
+ Text(k,
573
+ style: const TextStyle(
574
+ color: _C.faint, fontFamily: _mono, fontSize: 10)),
575
+ const SizedBox(height: 2),
576
+ Text(v,
577
+ overflow: TextOverflow.ellipsis,
578
+ style: const TextStyle(
579
+ color: _C.text, fontFamily: _mono, fontSize: 12)),
580
+ ],
581
+ ),
582
+ );
583
+
584
+ Widget _dot(Color c) => Container(
585
+ width: 11,
586
+ height: 11,
587
+ decoration: BoxDecoration(color: c, shape: BoxShape.circle),
588
+ );
589
+
590
+ Widget _pill(String t, Color c) => Container(
591
+ padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3),
592
+ decoration: BoxDecoration(
593
+ color: c.withValues(alpha: 0.15),
594
+ borderRadius: BorderRadius.circular(6),
595
+ border: Border.all(color: c.withValues(alpha: 0.5)),
596
+ ),
597
+ child: Text(t,
598
+ style: TextStyle(
599
+ color: c,
600
+ fontFamily: _mono,
601
+ fontSize: 10,
602
+ fontWeight: FontWeight.w700,
603
+ letterSpacing: 0.5)),
604
+ );
605
+ }
606
+
607
+ /// Scrollable terminal log view — auto-scrolls to bottom on new lines.
608
+ ///
609
+ /// A line whose text has a leading token followed by 2+ spaces is rendered
610
+ /// with the token in its tag color and the remainder in the body color.
611
+ class _TerminalLog extends StatefulWidget {
612
+ final List<LogLine> logs;
613
+
614
+ const _TerminalLog({required this.logs});
615
+
616
+ @override
617
+ State<_TerminalLog> createState() => _TerminalLogState();
618
+ }
619
+
620
+ class _TerminalLogState extends State<_TerminalLog> {
621
+ final _controller = ScrollController();
622
+
623
+ @override
624
+ void didUpdateWidget(covariant _TerminalLog oldWidget) {
625
+ super.didUpdateWidget(oldWidget);
626
+ if (widget.logs.length > oldWidget.logs.length) {
627
+ WidgetsBinding.instance.addPostFrameCallback((_) {
628
+ if (_controller.hasClients) {
629
+ _controller.animateTo(
630
+ _controller.position.maxScrollExtent,
631
+ duration: const Duration(milliseconds: 200),
632
+ curve: Curves.easeOut,
633
+ );
634
+ }
635
+ });
636
+ }
637
+ }
638
+
639
+ @override
640
+ void dispose() {
641
+ _controller.dispose();
642
+ super.dispose();
643
+ }
644
+
645
+ @override
646
+ Widget build(BuildContext context) {
647
+ final logs = widget.logs.isEmpty
648
+ ? const [LogLine('gray', 'status waiting for events…')]
649
+ : widget.logs;
650
+ return ListView.builder(
651
+ controller: _controller,
652
+ itemCount: logs.length,
653
+ itemBuilder: (context, i) {
654
+ final line = logs[i];
655
+ final color = _logColors[line.color] ?? _C.dim;
656
+
657
+ // Split "tag rest" (2+ spaces) into an accent tag + body.
658
+ final match = RegExp(r'^(\S+)(\s{2,})(.*)$').firstMatch(line.text);
659
+ final Widget content;
660
+ if (match != null) {
661
+ content = RichText(
662
+ text: TextSpan(
663
+ style: const TextStyle(fontFamily: _mono, fontSize: 12),
664
+ children: [
665
+ TextSpan(
666
+ text: '${match.group(1)!.padRight(8)} ',
667
+ style: TextStyle(color: color)),
668
+ TextSpan(
669
+ text: match.group(3),
670
+ style: const TextStyle(color: _C.text)),
671
+ ],
672
+ ),
673
+ );
674
+ } else {
675
+ content = Text(
676
+ line.text,
677
+ style: TextStyle(
678
+ fontFamily: _mono, fontSize: 12, color: color),
679
+ );
680
+ }
681
+
682
+ return Padding(
683
+ padding: const EdgeInsets.only(bottom: 3),
684
+ child: content,
685
+ );
686
+ },
687
+ );
688
+ }
689
+ }
690
+
691
+ /// Drives the forced-update progress overlay from SDK code.
692
+ class OtaOverlayManager {
693
+ OtaOverlayManager._();
694
+
695
+ static final OtaOverlayManager instance = OtaOverlayManager._();
696
+
697
+ OverlayState? _overlayState;
698
+ final ValueNotifier<OtaOverlayState> _state = ValueNotifier(
699
+ const OtaOverlayState(),
700
+ );
701
+ OverlayEntry? _entry;
702
+ bool _disposed = false;
703
+ DateTime? _startTime;
704
+
705
+ // Download-speed tracking.
706
+ int _lastBytes = 0;
707
+ DateTime? _lastBytesAt;
708
+
709
+ OverlayState? Function()? _resolver;
710
+
711
+ /// Minimum time the overlay stays visible (7 seconds).
712
+ static const Duration _minDuration = Duration(seconds: 7);
713
+
714
+ /// Dwell time after patch is applied before restart.
715
+ static const Duration _successDwell = Duration(seconds: 2);
716
+
717
+ static const Duration errorDwell = Duration(seconds: 8);
718
+
719
+ void setResolver(OverlayState? Function()? resolver) => _resolver = resolver;
720
+
721
+ void register(OverlayState state) => _overlayState = state;
722
+
723
+ void unregister(OverlayState state) {
724
+ if (_overlayState == state) _overlayState = null;
725
+ }
726
+
727
+ OtaOverlayHandle? begin({
728
+ String? message,
729
+ String? commitMessage,
730
+ String? targetVersion,
731
+ String? currentVersion,
732
+ String? errorHint,
733
+ String? bundleHash,
734
+ String? gitCommit,
735
+ String? channel,
736
+ String? platform,
737
+ }) {
738
+ if (_disposed) return null;
739
+ final overlay = _overlayState ?? _resolver?.call();
740
+ if (overlay == null) return null;
741
+
742
+ _startTime = DateTime.now();
743
+ _lastBytes = 0;
744
+ _lastBytesAt = null;
745
+
746
+ final logs = <LogLine>[
747
+ if (channel != null) LogLine('gray', 'channel $channel'),
748
+ if (platform != null) LogLine('gray', 'platform $platform'),
749
+ if (bundleHash != null) LogLine('gray', 'bundle $bundleHash'),
750
+ if (gitCommit != null) LogLine('gray', 'commit $gitCommit'),
751
+ if (commitMessage != null && commitMessage.isNotEmpty)
752
+ LogLine('cyan', 'msg $commitMessage'),
753
+ if (message != null && message.isNotEmpty)
754
+ LogLine('white', 'deploy $message'),
755
+ ];
756
+
757
+ _state.value = OtaOverlayState(
758
+ message: message,
759
+ commitMessage: commitMessage,
760
+ targetVersion: targetVersion,
761
+ currentVersion: currentVersion,
762
+ errorHint: errorHint,
763
+ bundleHash: bundleHash,
764
+ gitCommit: gitCommit,
765
+ channel: channel,
766
+ platform: platform,
767
+ logs: logs,
768
+ activeStep: 0,
769
+ );
770
+ _entry = OverlayEntry(builder: (_) => OtaProgressOverlay(state: _state));
771
+ overlay.insert(_entry!);
772
+ return OtaOverlayHandle._(this);
773
+ }
774
+
775
+ void _log(String color, String text) {
776
+ if (_disposed) return;
777
+ final current = _state.value;
778
+ _state.value = current.copyWith(
779
+ logs: [...current.logs, LogLine(color, text)],
780
+ );
781
+ }
782
+
783
+ /// Map phase to step index.
784
+ int _phaseToStep(PatchApplyPhase? phase) {
785
+ switch (phase) {
786
+ case PatchApplyPhase.downloading:
787
+ return 1;
788
+ case PatchApplyPhase.verifying:
789
+ return 2;
790
+ case PatchApplyPhase.finalizing:
791
+ return 3;
792
+ case null:
793
+ return 0;
794
+ }
795
+ }
796
+
797
+ void _update(PatchApplyProgress progress) {
798
+ if (_disposed || _entry == null) return;
799
+ final s = _state.value;
800
+ final pct = progress.fraction;
801
+ final pctText = pct != null
802
+ ? '${(pct.clamp(0.0, 1.0) * 100).toStringAsFixed(0)}%'
803
+ : '';
804
+
805
+ final newStep = _phaseToStep(progress.phase);
806
+
807
+ // Compute download speed from byte deltas.
808
+ int? bytesPerSec = s.bytesPerSec;
809
+ if (progress.phase == PatchApplyPhase.downloading &&
810
+ progress.bytesReceived > 0) {
811
+ final now = DateTime.now();
812
+ if (_lastBytesAt != null && progress.bytesReceived > _lastBytes) {
813
+ final dt = now.difference(_lastBytesAt!).inMilliseconds / 1000.0;
814
+ if (dt > 0) {
815
+ bytesPerSec = ((progress.bytesReceived - _lastBytes) / dt).round();
816
+ }
817
+ }
818
+ _lastBytes = progress.bytesReceived;
819
+ _lastBytesAt = now;
820
+ }
821
+
822
+ // Log phase transitions with staggered delays.
823
+ if (s.phase != progress.phase) {
824
+ switch (progress.phase) {
825
+ case PatchApplyPhase.downloading:
826
+ _log('gray', 'status connecting…');
827
+ _delayedLog(300, 'gray', 'status fetching metadata…');
828
+ _delayedLog(700, 'cyan', 'resolve download url');
829
+ _delayedLog(1100, 'gray', 'status downloading…');
830
+ break;
831
+ case PatchApplyPhase.verifying:
832
+ _log('cyan', 'verify checking hash…');
833
+ _delayedLog(400, 'cyan', 'verify checking signature…');
834
+ _delayedLog(800, 'success', 'verify passed ✓');
835
+ break;
836
+ case PatchApplyPhase.finalizing:
837
+ _log('gray', 'status staging files…');
838
+ _delayedLog(500, 'gray', 'status installing…');
839
+ _delayedLog(1000, 'gray', 'status finalizing…');
840
+ break;
841
+ }
842
+ }
843
+
844
+ // Log download progress with size.
845
+ if (progress.phase == PatchApplyPhase.downloading &&
846
+ pctText.isNotEmpty &&
847
+ progress.totalBytes > 0) {
848
+ final sizeMB = (progress.totalBytes / 1048576).toStringAsFixed(1);
849
+ _log('gray', 'download ${sizeMB}MB $pctText');
850
+ }
851
+
852
+ _state.value = _state.value.copyWith(
853
+ phase: progress.phase,
854
+ fraction: progress.fraction,
855
+ bytesReceived: progress.bytesReceived,
856
+ totalBytes: progress.totalBytes > 0 ? progress.totalBytes : null,
857
+ bytesPerSec: bytesPerSec,
858
+ activeStep: newStep,
859
+ );
860
+ }
861
+
862
+ /// Log with a delay for staged feel.
863
+ void _delayedLog(int ms, String color, String text) {
864
+ Future.delayed(Duration(milliseconds: ms), () {
865
+ if (_disposed) return;
866
+ _log(color, text);
867
+ });
868
+ }
869
+
870
+ void _end({bool hasError = false, String? errorText}) {
871
+ if (_disposed || _entry == null) return;
872
+ if (hasError) {
873
+ _log('red', 'error ${errorText ?? 'unknown'}');
874
+ _log('yellow', 'action rolling back to base apk');
875
+ } else {
876
+ _log('success', 'done patch installed ✓');
877
+ _delayedLog(300, 'cyan', 'status restarting…');
878
+ }
879
+
880
+ // Calculate remaining time to enforce minimum duration.
881
+ final elapsed = _startTime != null
882
+ ? DateTime.now().difference(_startTime!)
883
+ : _minDuration;
884
+ final remaining = _minDuration - elapsed;
885
+ final delay = remaining.isNegative ? Duration.zero : remaining;
886
+
887
+ _state.value = _state.value.copyWith(
888
+ hasError: hasError,
889
+ errorText: errorText,
890
+ canRetry: hasError,
891
+ activeStep: hasError ? _state.value.activeStep : 4,
892
+ );
893
+
894
+ final entry = _entry;
895
+ final dwell = hasError ? errorDwell : _successDwell + delay;
896
+ Future.delayed(dwell, () {
897
+ if (_disposed) return;
898
+ entry?.remove();
899
+ if (identical(_entry, entry)) _entry = null;
900
+ });
901
+ }
902
+
903
+ /// Shows a toast notification on the forced-update overlay when a rollback
904
+ /// occurs.
905
+ void showRollbackToast({
906
+ required String message,
907
+ required String previousVersion,
908
+ }) {
909
+ if (_disposed) return;
910
+ final overlay = _overlayState ?? _resolver?.call();
911
+ if (overlay == null) return;
912
+
913
+ final toastEntry = OverlayEntry(
914
+ builder: (context) => Positioned(
915
+ bottom: 100,
916
+ left: 24,
917
+ right: 24,
918
+ child: Material(
919
+ color: Colors.transparent,
920
+ child: Container(
921
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
922
+ decoration: BoxDecoration(
923
+ color: Colors.black.withValues(alpha: 0.85),
924
+ borderRadius: BorderRadius.circular(12),
925
+ border: Border.all(color: Colors.orange.withValues(alpha: 0.5)),
926
+ boxShadow: [
927
+ BoxShadow(
928
+ color: Colors.black.withValues(alpha: 0.3),
929
+ blurRadius: 12,
930
+ offset: const Offset(0, 4),
931
+ ),
932
+ ],
933
+ ),
934
+ child: Row(
935
+ mainAxisSize: MainAxisSize.min,
936
+ children: [
937
+ const Icon(Icons.info_outline, color: Colors.orange, size: 20),
938
+ const SizedBox(width: 12),
939
+ Flexible(
940
+ child: Column(
941
+ mainAxisSize: MainAxisSize.min,
942
+ crossAxisAlignment: CrossAxisAlignment.start,
943
+ children: [
944
+ Text(
945
+ message,
946
+ style: const TextStyle(
947
+ color: Colors.white,
948
+ fontSize: 14,
949
+ fontWeight: FontWeight.w500,
950
+ ),
951
+ ),
952
+ Text(
953
+ 'Restored: $previousVersion',
954
+ style: TextStyle(
955
+ color: Colors.grey[400],
956
+ fontSize: 12,
957
+ ),
958
+ ),
959
+ ],
960
+ ),
961
+ ),
962
+ ],
963
+ ),
964
+ ),
965
+ ),
966
+ ),
967
+ );
968
+
969
+ overlay.insert(toastEntry);
970
+ Future.delayed(const Duration(seconds: 4), () {
971
+ if (!_disposed) toastEntry.remove();
972
+ });
973
+ }
974
+ }
975
+
976
+ /// Handle returned by [OtaOverlayManager.begin] to update / dismiss the
977
+ /// overlay.
978
+ class OtaOverlayHandle {
979
+ OtaOverlayHandle._(this._manager);
980
+
981
+ final OtaOverlayManager _manager;
982
+
983
+ void update(PatchApplyProgress progress) => _manager._update(progress);
984
+
985
+ void end({bool hasError = false, String? errorText}) =>
986
+ _manager._end(hasError: hasError, errorText: errorText);
987
+ }