@_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.
- package/README.md +112 -18
- package/bin/flutter-ota-linux-x64 +0 -0
- package/bin/flutter-ota.js +12 -11
- package/dart-src/packages/cli-tools/README.md +146 -2
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +46 -13
- package/dart-src/packages/cli-tools/lib/src/backend.dart +6 -6
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +2 -0
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +7 -7
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +4 -1
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +36 -8
- package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/config.dart +5 -5
- package/dart-src/packages/cli-tools/lib/src/operations.dart +2 -2
- package/dart-src/packages/cli-tools/pubspec.yaml +8 -21
- package/dart-src/packages/cli-tools/test/backends_test.dart +3 -3
- package/dart-src/packages/cli-tools/test/cli_test.dart +1 -1
- package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +4 -1
- package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +2 -2
- package/dart-src/packages/cli-tools/test/pocketbase_smoke_test.dart +1 -1
- package/dart-src/root/README.md +334 -0
- package/dart-src/root/analysis_options.yaml +21 -0
- package/dart-src/root/android/build.gradle +116 -0
- package/dart-src/root/android/consumer-rules.pro +23 -0
- package/dart-src/root/android/settings.gradle +1 -0
- package/dart-src/root/android/src/main/AndroidManifest.xml +21 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/ApplyResult.kt +52 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/BlacklistStore.kt +154 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/BootDiagnosticStore.kt +150 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/CrashGuard.kt +271 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/FlutterPatcherApplication.kt +201 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/FlutterPatcherAutoInitProvider.kt +93 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/FlutterPatcherPlugin.kt +245 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/LoaderHook.kt +298 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatchManager.kt +1579 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatcherConfig.kt +173 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/SignatureVerifier.kt +177 -0
- package/dart-src/root/android/src/test/kotlin/com/flutter_patcher/flutter_patcher/CrashGuardTest.kt +61 -0
- package/dart-src/root/android/src/test/kotlin/com/flutter_patcher/flutter_patcher/LoaderHookTest.kt +25 -0
- package/dart-src/root/android/src/test/kotlin/com/flutter_patcher/flutter_patcher/PatchManagerTest.kt +401 -0
- package/dart-src/root/android/src/test/kotlin/com/flutter_patcher/flutter_patcher/PatcherConfigTest.kt +30 -0
- package/dart-src/root/bin/pack.dart +182 -0
- package/dart-src/root/lib/flutter_ota_kit.dart +1170 -0
- package/dart-src/root/lib/src/app_version_resolver.dart +36 -0
- package/dart-src/root/lib/src/aws_update_source.dart +97 -0
- package/dart-src/root/lib/src/blacklist.dart +54 -0
- package/dart-src/root/lib/src/boot_diagnostic.dart +138 -0
- package/dart-src/root/lib/src/cloudflare_update_source.dart +97 -0
- package/dart-src/root/lib/src/flutter_ota_app.dart +69 -0
- package/dart-src/root/lib/src/io.dart +48 -0
- package/dart-src/root/lib/src/io_stub.dart +19 -0
- package/dart-src/root/lib/src/ota_progress_overlay.dart +1186 -0
- package/dart-src/root/lib/src/pack/pack.dart +434 -0
- package/dart-src/root/lib/src/patch_info.dart +12 -0
- package/dart-src/root/lib/src/patcher_channel.dart +160 -0
- package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_database.dart +1 -1
- package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_s3_client.dart +1 -1
- package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_storage.dart +1 -1
- package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_storage_profile.dart +1 -1
- package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/with_cloudfront_signed_url.dart +1 -1
- package/dart-src/root/lib/src/pkg/client/flutter_ota_kit_client.dart +1 -0
- package/dart-src/root/lib/src/pkg/client/src/server_update_source.dart +46 -0
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/cloudflare_worker_database.dart +1 -1
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_build_where.dart +1 -1
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_bundle_mapper.dart +3 -3
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_database.dart +1 -1
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_database_plugin.dart +2 -2
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/r2_s3_client.dart +1 -1
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/r2_storage.dart +1 -1
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/r2_storage_profile.dart +1 -1
- package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/status.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/flutter_ota_kit_plugin_core.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/create_blob_database_plugin.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/filter_compatible_app_versions.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/get_update_info.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/request_update_bundle_state.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/resolve_update_info_from_bundles.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/types.dart +2 -2
- package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_bundle_mapper.dart +2 -2
- package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_database.dart +26 -15
- package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_storage.dart +1 -1
- package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_bundle_mapper.dart +2 -2
- package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_database.dart +2 -2
- package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_get_update_info.dart +2 -2
- package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_storage.dart +1 -1
- package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_types.dart +1 -1
- package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_bundle_mapper.dart +2 -2
- package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_database.dart +2 -2
- package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_edge_function_database.dart +1 -1
- package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_edge_function_storage.dart +1 -1
- package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_storage.dart +1 -1
- package/dart-src/root/lib/src/pocketbase_update_source.dart +107 -0
- package/dart-src/root/lib/src/postgres_update_source.dart +95 -0
- package/dart-src/root/lib/src/shared_update_check.dart +131 -0
- package/dart-src/root/lib/src/supabase_update_source.dart +93 -0
- package/dart-src/root/pubspec.yaml +54 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +17 -13
- package/scripts/vendor.sh +22 -7
- package/dart-src/packages/core/CHANGELOG.md +0 -21
- package/dart-src/packages/core/README.md +0 -23
- package/dart-src/packages/core/example/lib/main.dart +0 -69
- package/dart-src/packages/core/pubspec.yaml +0 -13
- package/dart-src/packages/core/test/rollout_test.dart +0 -135
- package/dart-src/packages/core/test/semver_test.dart +0 -55
- package/dart-src/packages/core/test/semver_vectors.json +0 -1
- package/dart-src/packages/core/test/types_test.dart +0 -131
- package/dart-src/packages/core/test/uuid_test.dart +0 -40
- package/dart-src/packages/core/tool/dbg.dart +0 -7
- package/dart-src/plugins/aws/.gitkeep +0 -0
- package/dart-src/plugins/aws/CHANGELOG.md +0 -23
- package/dart-src/plugins/aws/LICENSE +0 -21
- package/dart-src/plugins/aws/README.md +0 -28
- package/dart-src/plugins/aws/example/lib/main.dart +0 -60
- package/dart-src/plugins/aws/pubspec.yaml +0 -30
- package/dart-src/plugins/aws/test/aws_database_test.dart +0 -161
- package/dart-src/plugins/aws/test/aws_storage_test.dart +0 -73
- package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +0 -78
- package/dart-src/plugins/cloudflare/.gitkeep +0 -0
- package/dart-src/plugins/cloudflare/CHANGELOG.md +0 -23
- package/dart-src/plugins/cloudflare/LICENSE +0 -21
- package/dart-src/plugins/cloudflare/README.md +0 -25
- package/dart-src/plugins/cloudflare/example/lib/main.dart +0 -62
- package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +0 -16
- package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +0 -8
- package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +0 -38
- package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +0 -11
- package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +0 -24
- package/dart-src/plugins/cloudflare/pubspec.yaml +0 -28
- package/dart-src/plugins/cloudflare/sql/bundles.sql +0 -42
- package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +0 -126
- package/dart-src/plugins/cloudflare/test/d1_bundle_mapper_test.dart +0 -106
- package/dart-src/plugins/cloudflare/test/d1_database_test.dart +0 -200
- package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +0 -261
- package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +0 -78
- package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +0 -74
- package/dart-src/plugins/plugin-core/CHANGELOG.md +0 -20
- package/dart-src/plugins/plugin-core/LICENSE +0 -21
- package/dart-src/plugins/plugin-core/README.md +0 -21
- package/dart-src/plugins/plugin-core/example/lib/main.dart +0 -61
- package/dart-src/plugins/plugin-core/pubspec.yaml +0 -23
- package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +0 -170
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_store_test.dart +0 -74
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +0 -345
- package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +0 -843
- package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +0 -298
- package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +0 -195
- package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +0 -56
- package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +0 -1589
- package/dart-src/plugins/plugin-core/test/helpers_test.dart +0 -283
- package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +0 -202
- package/dart-src/plugins/plugin-core/test/request_update_bundle_state_test.dart +0 -86
- package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +0 -88
- package/dart-src/plugins/pocketbase/CHANGELOG.md +0 -23
- package/dart-src/plugins/pocketbase/LICENSE +0 -21
- package/dart-src/plugins/pocketbase/README.md +0 -54
- package/dart-src/plugins/pocketbase/example/lib/main.dart +0 -62
- package/dart-src/plugins/pocketbase/pubspec.yaml +0 -26
- package/dart-src/plugins/pocketbase/test/pb_uri_test.dart +0 -12
- package/dart-src/plugins/postgres/.gitkeep +0 -0
- package/dart-src/plugins/postgres/CHANGELOG.md +0 -23
- package/dart-src/plugins/postgres/LICENSE +0 -21
- package/dart-src/plugins/postgres/README.md +0 -29
- package/dart-src/plugins/postgres/example/lib/main.dart +0 -67
- package/dart-src/plugins/postgres/pubspec.yaml +0 -26
- package/dart-src/plugins/postgres/sql/bundles.sql +0 -45
- package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +0 -21
- package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +0 -171
- package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +0 -126
- package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +0 -125
- package/dart-src/plugins/postgres/sql/hash_user_id.sql +0 -30
- package/dart-src/plugins/postgres/sql/is_device_eligible.sql +0 -237
- package/dart-src/plugins/postgres/sql/prepareSql.ts +0 -11
- package/dart-src/plugins/postgres/test/mock_postgres_client.dart +0 -320
- package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +0 -139
- package/dart-src/plugins/postgres/test/postgres_database_test.dart +0 -210
- package/dart-src/plugins/postgres/test/postgres_storage_runtime_test.dart +0 -60
- package/dart-src/plugins/supabase/CHANGELOG.md +0 -24
- package/dart-src/plugins/supabase/LICENSE +0 -21
- package/dart-src/plugins/supabase/README.md +0 -53
- package/dart-src/plugins/supabase/example/lib/main.dart +0 -63
- package/dart-src/plugins/supabase/pubspec.yaml +0 -27
- package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +0 -44
- package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +0 -968
- package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +0 -232
- package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +0 -137
- package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +0 -206
- package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +0 -186
- package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +0 -505
- package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +0 -50
- package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +0 -20
- package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +0 -75
- package/dart-src/plugins/supabase/test/mock_supabase_client.dart +0 -654
- package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +0 -115
- package/dart-src/plugins/supabase/test/supabase_database_test.dart +0 -198
- package/dart-src/plugins/supabase/test/supabase_storage_test.dart +0 -168
- /package/dart-src/{packages/core → root}/LICENSE +0 -0
- /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/flutter_ota_kit_aws.dart +0 -0
- /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_cloudfront_client.dart +0 -0
- /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_cloudfront_signer.dart +0 -0
- /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_config.dart +0 -0
- /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_ssm_client.dart +0 -0
- /package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/flutter_ota_kit_cloudflare.dart +0 -0
- /package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_client.dart +0 -0
- /package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_config.dart +0 -0
- /package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/r2_config.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/flutter_ota_kit_core.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/app_update_info.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/bundle.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/bundle_artifacts.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/bundle_patch_artifact.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/changed_asset.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/get_bundles_args.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/metadata.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/patch_info.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/platform.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/rollout.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/semver.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/semver_range.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/semver_version.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/strategy.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/update_bundle_params.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/uuid.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/asset_storage_layout.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/bundle_helpers.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/bundle_storage_layout.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/bundle_unit_of_work.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/bundle_unit_of_work_store.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/calculate_pagination.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/compression_format.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/content_addressed_assets.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/create_database_plugin.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/create_storage_key_builder.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/create_storage_plugin.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/generate_min_bundle_id.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/is_object.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/legacy_asset_storage_layout.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/paginate_bundles.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/parse_storage_uri.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/query_bundles.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/storage_helpers.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/storage_profile.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/uuidv7.dart +0 -0
- /package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/flutter_ota_kit_pocketbase.dart +0 -0
- /package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_client.dart +0 -0
- /package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_config.dart +0 -0
- /package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/flutter_ota_kit_postgres.dart +0 -0
- /package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_client.dart +0 -0
- /package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_config.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/edge.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/flutter_ota_kit_supabase.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/error_message.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_client_adapter.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_client_http.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_config.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_signed_url_batcher.dart +0 -0
- /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
|
+
}
|