@_nazmiforreal/flutter-ota 0.1.26 → 0.1.28

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