From e29f1897a3659513ec4ffe03cf327103f871f94c Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 25 Apr 2022 17:12:25 +0900 Subject: [PATCH] huawei mobile services --- .github/workflows/release.yml | 3 + CHANGELOG.md | 1 + android/app/agconnect-services.json | 75 ++++ android/app/build.gradle | 10 + .../aves/channel/calls/DebugHandler.kt | 2 + .../aves/channel/calls/EmbeddedDataHandler.kt | 2 + .../channel/calls/MetadataFetchHandler.kt | 17 +- .../thibault/aves/metadata/MultiPage.kt | 2 + .../thibault/aves/model/SourceEntry.kt | 2 + .../model/provider/ContentImageProvider.kt | 2 + android/build.gradle | 6 +- lib/app_flavor.dart | 12 +- lib/l10n/app_de.arb | 2 + lib/l10n/app_en.arb | 2 + lib/l10n/app_es.arb | 8 +- lib/l10n/app_fr.arb | 2 + lib/l10n/app_id.arb | 2 + lib/l10n/app_it.arb | 2 + lib/l10n/app_ja.arb | 2 + lib/l10n/app_ko.arb | 8 +- lib/l10n/app_pt.arb | 2 + lib/l10n/app_ru.arb | 2 + lib/l10n/app_zh.arb | 8 +- lib/main_huawei.dart | 6 + lib/model/availability.dart | 27 +- lib/model/filters/coordinate.dart | 2 +- lib/model/geotiff.dart | 15 +- lib/model/settings/defaults.dart | 1 + .../settings/enums/coordinate_format.dart | 23 +- lib/model/settings/enums/enums.dart | 3 - lib/model/settings/enums/map_style.dart | 22 +- lib/model/settings/settings.dart | 10 +- lib/utils/constants.dart | 45 ++- lib/widgets/about/bug_report.dart | 5 +- lib/widgets/aves_app.dart | 5 +- lib/widgets/common/map/attribution.dart | 2 +- lib/widgets/common/map/buttons/button.dart | 55 +++ .../common/map/buttons/coordinate_filter.dart | 111 ++++++ .../map/{buttons.dart => buttons/panel.dart} | 175 +--------- lib/widgets/common/map/decorator.dart | 2 +- lib/widgets/common/map/geo_map.dart | 195 ++++++----- .../map/google/geotiff_tile_provider.dart | 17 - .../map/{ => leaflet}/latlng_tween.dart | 2 +- .../map/{ => leaflet}/latlng_utils.dart | 0 lib/widgets/common/map/leaflet/map.dart | 57 ++- .../map_theme_provider.dart} | 20 +- lib/widgets/dialogs/location_pick_dialog.dart | 8 +- lib/widgets/map/map_info_row.dart | 2 +- lib/widgets/map/map_page.dart | 14 +- lib/widgets/viewer/info/location_section.dart | 4 +- plugins/aves_map/.gitignore | 29 ++ plugins/aves_map/.metadata | 10 + plugins/aves_map/analysis_options.yaml | 1 + plugins/aves_map/lib/aves_map.dart | 15 + .../aves_map/lib/src}/controller.dart | 2 +- .../aves_map/lib/src}/geo_entry.dart | 5 +- .../aves_map/lib/src}/geo_utils.dart | 19 - plugins/aves_map/lib/src/interface.dart | 12 + plugins/aves_map/lib/src/marker/dot.dart | 54 +++ .../aves_map/lib/src/marker/generator.dart | 0 .../aves_map/lib/src/marker/image.dart | 88 +---- plugins/aves_map/lib/src/marker/key.dart | 13 + plugins/aves_map/lib/src/overlay/overlay.dart | 17 + .../aves_map/lib/src/overlay}/tile.dart | 0 plugins/aves_map/lib/src/style.dart | 14 + plugins/aves_map/lib/src/theme.dart | 29 ++ .../aves_map/lib/src}/zoomed_bounds.dart | 2 +- plugins/aves_map/pubspec.yaml | 22 ++ plugins/aves_report/pubspec.yaml | 3 +- plugins/aves_report_console/pubspec.yaml | 3 +- plugins/aves_report_crashlytics/pubspec.yaml | 3 +- plugins/aves_services/.gitignore | 29 ++ plugins/aves_services/.metadata | 10 + plugins/aves_services/analysis_options.yaml | 1 + plugins/aves_services/lib/aves_services.dart | 31 ++ plugins/aves_services/pubspec.yaml | 18 + plugins/aves_services_google/.gitignore | 29 ++ plugins/aves_services_google/.metadata | 10 + .../analysis_options.yaml | 1 + .../lib/aves_services_platform.dart | 67 ++++ .../aves_services_google/lib/src}/map.dart | 127 ++++--- plugins/aves_services_google/pubspec.yaml | 23 ++ plugins/aves_services_huawei/.gitignore | 29 ++ plugins/aves_services_huawei/.metadata | 10 + .../analysis_options.yaml | 1 + .../lib/aves_services_platform.dart | 70 ++++ plugins/aves_services_huawei/lib/src/map.dart | 325 ++++++++++++++++++ plugins/aves_services_huawei/pubspec.yaml | 23 ++ pubspec.lock | 35 +- pubspec.yaml | 12 +- scripts/apply_flavor_huawei.sh | 9 + scripts/apply_flavor_izzy.sh | 1 + scripts/apply_flavor_play.sh | 1 + test/utils/geo_utils_test.dart | 2 +- test_driver/driver_screenshots.dart | 1 + test_driver/driver_shaders.dart | 1 + 96 files changed, 1613 insertions(+), 596 deletions(-) create mode 100644 android/app/agconnect-services.json create mode 100644 lib/main_huawei.dart create mode 100644 lib/widgets/common/map/buttons/button.dart create mode 100644 lib/widgets/common/map/buttons/coordinate_filter.dart rename lib/widgets/common/map/{buttons.dart => buttons/panel.dart} (56%) delete mode 100644 lib/widgets/common/map/google/geotiff_tile_provider.dart rename lib/widgets/common/map/{ => leaflet}/latlng_tween.dart (83%) rename lib/widgets/common/map/{ => leaflet}/latlng_utils.dart (100%) rename lib/widgets/common/{map/theme.dart => providers/map_theme_provider.dart} (70%) create mode 100644 plugins/aves_map/.gitignore create mode 100644 plugins/aves_map/.metadata create mode 100644 plugins/aves_map/analysis_options.yaml create mode 100644 plugins/aves_map/lib/aves_map.dart rename {lib/widgets/common/map => plugins/aves_map/lib/src}/controller.dart (95%) rename {lib/widgets/common/map => plugins/aves_map/lib/src}/geo_entry.dart (92%) rename {lib/utils => plugins/aves_map/lib/src}/geo_utils.dart (89%) create mode 100644 plugins/aves_map/lib/src/interface.dart create mode 100644 plugins/aves_map/lib/src/marker/dot.dart rename lib/widgets/common/map/google/marker_generator.dart => plugins/aves_map/lib/src/marker/generator.dart (100%) rename lib/widgets/common/map/marker.dart => plugins/aves_map/lib/src/marker/image.dart (65%) create mode 100644 plugins/aves_map/lib/src/marker/key.dart create mode 100644 plugins/aves_map/lib/src/overlay/overlay.dart rename {lib/widgets/common/map => plugins/aves_map/lib/src/overlay}/tile.dart (100%) create mode 100644 plugins/aves_map/lib/src/style.dart create mode 100644 plugins/aves_map/lib/src/theme.dart rename {lib/widgets/common/map => plugins/aves_map/lib/src}/zoomed_bounds.dart (98%) create mode 100644 plugins/aves_map/pubspec.yaml create mode 100644 plugins/aves_services/.gitignore create mode 100644 plugins/aves_services/.metadata create mode 100644 plugins/aves_services/analysis_options.yaml create mode 100644 plugins/aves_services/lib/aves_services.dart create mode 100644 plugins/aves_services/pubspec.yaml create mode 100644 plugins/aves_services_google/.gitignore create mode 100644 plugins/aves_services_google/.metadata create mode 100644 plugins/aves_services_google/analysis_options.yaml create mode 100644 plugins/aves_services_google/lib/aves_services_platform.dart rename {lib/widgets/common/map/google => plugins/aves_services_google/lib/src}/map.dart (73%) create mode 100644 plugins/aves_services_google/pubspec.yaml create mode 100644 plugins/aves_services_huawei/.gitignore create mode 100644 plugins/aves_services_huawei/.metadata create mode 100644 plugins/aves_services_huawei/analysis_options.yaml create mode 100644 plugins/aves_services_huawei/lib/aves_services_platform.dart create mode 100644 plugins/aves_services_huawei/lib/src/map.dart create mode 100644 plugins/aves_services_huawei/pubspec.yaml create mode 100755 scripts/apply_flavor_huawei.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9127d49e..12d85684a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,6 +59,9 @@ jobs: cp build/app/outputs/bundle/playRelease/*.aab outputs flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_2.10.5.sksl.json cp build/app/outputs/apk/play/release/*.apk outputs + (cd scripts/; ./apply_flavor_huawei.sh) + flutter build apk -t lib/main_huawei.dart --flavor huawei --bundle-sksl-path shaders_2.10.5.sksl.json + cp build/app/outputs/apk/huawei/release/*.apk outputs (cd scripts/; ./apply_flavor_izzy.sh) flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_2.10.5.sksl.json cp build/app/outputs/apk/izzy/release/*.apk outputs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f360fa74..aa17d9c42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ### Changed - upgraded Flutter to stable v2.10.5 +- `huawei` flavor (Petal Maps, no Crashlytics) ## [v1.6.4] - 2022-04-19 diff --git a/android/app/agconnect-services.json b/android/app/agconnect-services.json new file mode 100644 index 000000000..876ecb775 --- /dev/null +++ b/android/app/agconnect-services.json @@ -0,0 +1,75 @@ +{ + "agcgw_all":{ + "CN":"connect-drcn.dbankcloud.cn", + "CN_back":"connect-drcn.hispace.hicloud.com", + "DE":"connect-dre.dbankcloud.cn", + "DE_back":"connect-dre.hispace.hicloud.com", + "RU":"connect-drru.hispace.dbankcloud.ru", + "RU_back":"connect-drru.hispace.dbankcloud.ru", + "SG":"connect-dra.dbankcloud.cn", + "SG_back":"connect-dra.hispace.hicloud.com" + }, + "client":{ + "cp_id":"2640082000020010713", + "product_id":"99536292102197525", + "client_id":"874325707927340288", + "client_secret":"DCAFAE5C0440ABDBD6DDB2B6EBD7D9B0870C10FCA64759CCD63020D168803AB5", + "project_id":"99536292102197525", + "app_id":"106014023", + "api_key":"DAEDAEzScQA5ri36P2NEiVPSFrOJeYZ0DbEJZMGJrBadW+QudBr5BGHD3vO0tsL1VeBy0RPZefPic3hAWUijcBxCv0zRv0iBjQEptQ==", + "package_name":"deckers.thibault.aves" + }, + "oauth_client":{ + "client_id":"106014023", + "client_type":1 + }, + "app_info":{ + "app_id":"106014023", + "package_name":"deckers.thibault.aves" + }, + "configuration_version":"3.0", + "appInfos":[ + { + "package_name":"deckers.thibault.aves.profile", + "client":{ + "app_id":"106031461" + }, + "app_info":{ + "package_name":"deckers.thibault.aves.profile", + "app_id":"106031461" + }, + "oauth_client":{ + "client_type":1, + "client_id":"106031461" + } + }, + { + "package_name":"deckers.thibault.aves.debug", + "client":{ + "app_id":"106014297" + }, + "app_info":{ + "package_name":"deckers.thibault.aves.debug", + "app_id":"106014297" + }, + "oauth_client":{ + "client_type":1, + "client_id":"106014297" + } + }, + { + "package_name":"deckers.thibault.aves", + "client":{ + "app_id":"106014023" + }, + "app_info":{ + "package_name":"deckers.thibault.aves", + "app_id":"106014023" + }, + "oauth_client":{ + "client_type":1, + "client_id":"106014023" + } + } + ] +} \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 7b9253ca2..ff39e606d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,5 +1,6 @@ plugins { id 'com.android.application' + id 'com.huawei.agconnect' id 'kotlin-android' id 'kotlin-kapt' } @@ -85,6 +86,14 @@ android { ext.useNdkAbiFilters = true } + huawei { + // Huawei AppGallery + dimension "store" + ext.useCrashlytics = false + // generate a universal APK without x86 native libs + ext.useNdkAbiFilters = true + } + izzy { // IzzyOnDroid // check offending libraries with `scanapk` @@ -153,6 +162,7 @@ dependencies { // forked, built by JitPack, cf https://jitpack.io/p/deckerst/pixymeta-android implementation 'com.github.deckerst:pixymeta-android:706bd73d6e' implementation 'com.github.bumptech.glide:glide:4.13.1' + implementation 'com.huawei.agconnect:agconnect-core:1.5.2.300' kapt 'androidx.annotation:annotation:1.3.0' kapt 'com.github.bumptech.glide:compiler:4.13.0' diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt index 0d2061740..84f6db94f 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt @@ -308,6 +308,8 @@ class DebugHandler(private val context: Context) : MethodCallHandler { Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e) } } result.success(metadataMap) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt index 3b7d3a897..f09bf8ce1 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt @@ -175,6 +175,8 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { Log.w(LOG_TAG, "failed to extract file from XMP", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to extract file from XMP", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to extract file from XMP", e) } } result.error("extractXmpDataProp-empty", "failed to extract file from XMP uri=$uri prop=$dataPropPath", null) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt index a6ebfdfae..00f56f20c 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt @@ -331,6 +331,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } } @@ -601,6 +603,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } } @@ -727,6 +731,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } } @@ -784,6 +790,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } } result.error("getGeoTiffInfo-empty", "failed to get info for mimeType=$mimeType uri=$uri", null) @@ -844,6 +852,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } } result.error("getPanoramaInfo-empty", "failed to get info for mimeType=$mimeType uri=$uri", null) @@ -894,7 +904,10 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { result.error("getXmp-exception", "failed to read XMP for mimeType=$mimeType uri=$uri", e.message) return } catch (e: NoClassDefFoundError) { - result.error("getXmp-error", "failed to read XMP for mimeType=$mimeType uri=$uri", e.message) + result.error("getXmp-noclass", "failed to read XMP for mimeType=$mimeType uri=$uri", e.message) + return + } catch (e: AssertionError) { + result.error("getXmp-assert", "failed to read XMP for mimeType=$mimeType uri=$uri", e.message) return } } @@ -1031,6 +1044,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt index 241e50f02..e9a5dcabf 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt @@ -167,6 +167,8 @@ object MultiPage { Log.w(LOG_TAG, "failed to get motion photo offset from uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to get motion photo offset from uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to get motion photo offset from uri=$uri", e) } return null } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt index a355a8fa9..09406b448 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt @@ -204,6 +204,8 @@ class SourceEntry { // ignore } catch (e: NoClassDefFoundError) { // ignore + } catch (e: AssertionError) { + // ignore } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt index 13fa8dc2d..1781bba01 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt @@ -33,6 +33,8 @@ internal class ContentImageProvider : ImageProvider() { Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e) } val mimeType = extractorMimeType ?: sourceMimeType diff --git a/android/build.gradle b/android/build.gradle index ec015c9b9..8701470a4 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,9 +1,10 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.6.20' + ext.kotlin_version = '1.6.21' repositories { google() mavenCentral() + maven { url 'https://developer.huawei.com/repo/' } } dependencies { classpath 'com.android.tools.build:gradle:7.1.3' @@ -11,6 +12,8 @@ buildscript { // GMS & Firebase Crashlytics are not actually used by all flavors classpath 'com.google.gms:google-services:4.3.10' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' + // HMS + classpath 'com.huawei.agconnect:agcp:1.5.2.300' } } @@ -18,6 +21,7 @@ allprojects { repositories { google() mavenCentral() + maven {url 'https://developer.huawei.com/repo/'} } // gradle.projectsEvaluated { // tasks.withType(JavaCompile) { diff --git a/lib/app_flavor.dart b/lib/app_flavor.dart index 4dfcd54d6..6cd5520ae 100644 --- a/lib/app_flavor.dart +++ b/lib/app_flavor.dart @@ -1,5 +1,13 @@ -enum AppFlavor { play, izzy } +enum AppFlavor { play, huawei, izzy } extension ExtraAppFlavor on AppFlavor { - bool get canEnableErrorReporting => this == AppFlavor.play; + bool get canEnableErrorReporting { + switch (this) { + case AppFlavor.play: + return true; + case AppFlavor.huawei: + case AppFlavor.izzy: + return false; + } + } } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index c6977369b..8cff40d47 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -124,6 +124,8 @@ "mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleHybrid": "Google Maps (Hybrid)", "mapStyleGoogleTerrain": "Google Maps (Gelände)", + "mapStyleHuaweiNormal": "Petal Maps", + "mapStyleHuaweiTerrain": "Petal Maps (Gelände)", "mapStyleOsmHot": "Humanitäres OSM", "mapStyleStamenToner": "Stamen Toner (SchwarzWeiß)", "mapStyleStamenWatercolor": "Stamen Watercolor (Aquarell)", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 96359ab8d..be9d5a62a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -164,6 +164,8 @@ "mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleHybrid": "Google Maps (Hybrid)", "mapStyleGoogleTerrain": "Google Maps (Terrain)", + "mapStyleHuaweiNormal": "Petal Maps", + "mapStyleHuaweiTerrain": "Petal Maps (Terrain)", "mapStyleOsmHot": "Humanitarian OSM", "mapStyleStamenToner": "Stamen Toner", "mapStyleStamenWatercolor": "Stamen Watercolor", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 78ca559fc..15963a966 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -118,9 +118,11 @@ "videoControlsPlayOutside": "Reproducir externamente", "videoControlsNone": "Ninguno", - "mapStyleGoogleNormal": "Mapas de Google", - "mapStyleGoogleHybrid": "Mapas de Google (Híbrido)", - "mapStyleGoogleTerrain": "Mapas de Google (Superficie)", + "mapStyleGoogleNormal": "Google Maps", + "mapStyleGoogleHybrid": "Google Maps (Híbrido)", + "mapStyleGoogleTerrain": "Google Maps (Relieve)", + "mapStyleHuaweiNormal": "Petal Maps", + "mapStyleHuaweiTerrain": "Petal Maps (Relieve)", "mapStyleOsmHot": "OSM Humanitario", "mapStyleStamenToner": "Stamen Toner (Monocromático)", "mapStyleStamenWatercolor": "Stamen Watercolor (Acuarela)", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 574a709b1..f4a64aa0f 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -124,6 +124,8 @@ "mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleHybrid": "Google Maps (Satellite)", "mapStyleGoogleTerrain": "Google Maps (Relief)", + "mapStyleHuaweiNormal": "Petal Maps", + "mapStyleHuaweiTerrain": "Petal Maps (Relief)", "mapStyleOsmHot": "OSM Humanitaire", "mapStyleStamenToner": "Stamen Toner (Monochrome)", "mapStyleStamenWatercolor": "Stamen Watercolor (Aquarelle)", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 0547da17c..200639539 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -124,6 +124,8 @@ "mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleHybrid": "Google Maps (Hybrid)", "mapStyleGoogleTerrain": "Google Maps (Terrain)", + "mapStyleHuaweiNormal": "Petal Maps", + "mapStyleHuaweiTerrain": "Petal Maps (Terrain)", "mapStyleOsmHot": "Humanitarian OSM", "mapStyleStamenToner": "Stamen Toner", "mapStyleStamenWatercolor": "Stamen Watercolor", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 87a7d1eb4..fe2bf37c8 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -124,6 +124,8 @@ "mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleHybrid": "Google Maps (Ibrido)", "mapStyleGoogleTerrain": "Google Maps (Terreno)", + "mapStyleHuaweiNormal": "Petal Maps", + "mapStyleHuaweiTerrain": "Petal Maps (Terreno)", "mapStyleOsmHot": "OSM umanitario", "mapStyleStamenToner": "Stamen Toner (Monocromatico)", "mapStyleStamenWatercolor": "Stamen Watercolor (Acquerello)", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index cebf8e16a..649c5c35f 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -121,6 +121,8 @@ "mapStyleGoogleNormal": "Google マップ", "mapStyleGoogleHybrid": "Google マップ(ハイブリッド)", "mapStyleGoogleTerrain": "Google マップ(地形)", + "mapStyleHuaweiNormal": "Petal マップ", + "mapStyleHuaweiTerrain": "Petal マップ(地形)", "mapStyleOsmHot": "Humanitarian OSM", "mapStyleStamenToner": "Stamen Toner", "mapStyleStamenWatercolor": "Stamen Watercolor", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 6487b8352..0770c05ec 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -121,9 +121,11 @@ "videoControlsPlayOutside": "다른 앱에서 열기", "videoControlsNone": "없음", - "mapStyleGoogleNormal": "구글 지도", - "mapStyleGoogleHybrid": "구글 지도 (위성)", - "mapStyleGoogleTerrain": "구글 지도 (지형)", + "mapStyleGoogleNormal": "Google 지도", + "mapStyleGoogleHybrid": "Google 지도 (위성)", + "mapStyleGoogleTerrain": "Google 지도 (지형)", + "mapStyleHuaweiNormal": "Petal 지도", + "mapStyleHuaweiTerrain": "Petal 지도 (지형)", "mapStyleOsmHot": "Humanitarian OSM", "mapStyleStamenToner": "Stamen Toner (토너)", "mapStyleStamenWatercolor": "Stamen Watercolor (수채화)", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index abe1d5345..96a1e60c6 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -124,6 +124,8 @@ "mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleHybrid": "Google Maps (Híbrido)", "mapStyleGoogleTerrain": "Google Maps (Terreno)", + "mapStyleHuaweiNormal": "Petal Maps", + "mapStyleHuaweiTerrain": "Petal Maps (Terreno)", "mapStyleOsmHot": "OSM Humanitário", "mapStyleStamenToner": "Stamen Toner (Monocromático)", "mapStyleStamenWatercolor": "Stamen Watercolor (Aquarela)", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 5f3e9aee7..6fff9da86 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -124,6 +124,8 @@ "mapStyleGoogleNormal": "Google Карты", "mapStyleGoogleHybrid": "Google Карты (Гибридный)", "mapStyleGoogleTerrain": "Google Карты (Местность)", + "mapStyleHuaweiNormal": "Petal Карты", + "mapStyleHuaweiTerrain": "Petal Карты (Местность)", "mapStyleOsmHot": "Команда гуманитарной картопомощи", "mapStyleStamenToner": "Stamen Toner", "mapStyleStamenWatercolor": "Stamen Watercolor", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index f08d3ad1f..d40fcc833 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -121,9 +121,11 @@ "videoControlsPlayOutside": "用其他播放器打开", "videoControlsNone": "无", - "mapStyleGoogleNormal": "Google Maps", - "mapStyleGoogleHybrid": "Google Maps (Hybrid)", - "mapStyleGoogleTerrain": "Google Maps (Terrain)", + "mapStyleGoogleNormal": "Google 地图", + "mapStyleGoogleHybrid": "Google 地图 (卫星图像)", + "mapStyleGoogleTerrain": "Google 地图 (地形)", + "mapStyleHuaweiNormal": "Petal 地图", + "mapStyleHuaweiTerrain": "Petal 地图 (地形)", "mapStyleOsmHot": "Humanitarian OSM", "mapStyleStamenToner": "Stamen Toner", "mapStyleStamenWatercolor": "Stamen Watercolor", diff --git a/lib/main_huawei.dart b/lib/main_huawei.dart new file mode 100644 index 000000000..6475eb310 --- /dev/null +++ b/lib/main_huawei.dart @@ -0,0 +1,6 @@ +import 'package:aves/app_flavor.dart'; +import 'package:aves/main_common.dart'; + +void main() { + mainCommon(AppFlavor.huawei); +} diff --git a/lib/model/availability.dart b/lib/model/availability.dart index bdbb691fd..86f9c4640 100644 --- a/lib/model/availability.dart +++ b/lib/model/availability.dart @@ -1,22 +1,20 @@ import 'package:aves/model/device.dart'; +import 'package:aves_services_platform/aves_services_platform.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/foundation.dart'; -import 'package:google_api_availability/google_api_availability.dart'; abstract class AvesAvailability { void onResume(); Future get isConnected; - Future get hasPlayServices; - Future get canLocatePlaces; - Future get canUseGoogleMaps; + Future get canUseDeviceMaps; } class LiveAvesAvailability implements AvesAvailability { - bool? _isConnected, _hasPlayServices; + bool? _isConnected; LiveAvesAvailability() { Connectivity().onConnectivityChanged.listen(_updateConnectivityFromResult); @@ -41,19 +39,14 @@ class LiveAvesAvailability implements AvesAvailability { } } + // local geocoding with `geocoder` seems to require Google Play Services + // what about devices with Huawei Mobile Services? @override - Future get hasPlayServices async { - if (_hasPlayServices != null) return SynchronousFuture(_hasPlayServices!); - final result = await GoogleApiAvailability.instance.checkGooglePlayServicesAvailability(); - _hasPlayServices = result == GooglePlayServicesAvailability.success; - debugPrint('Device has Play Services=$_hasPlayServices'); - return _hasPlayServices!; - } - - // local geocoding with `geocoder` requires Play Services - @override - Future get canLocatePlaces => Future.wait([isConnected, hasPlayServices]).then((results) => results.every((result) => result)); + Future get canLocatePlaces => Future.wait([ + isConnected, + PlatformMobileServices().isServiceAvailable(), + ]).then((results) => results.every((result) => result)); @override - Future get canUseGoogleMaps async => device.canRenderGoogleMaps && await hasPlayServices; + Future get canUseDeviceMaps async => device.canRenderGoogleMaps && await PlatformMobileServices().isServiceAvailable(); } diff --git a/lib/model/filters/coordinate.dart b/lib/model/filters/coordinate.dart index 90664b8c0..5b42c7e41 100644 --- a/lib/model/filters/coordinate.dart +++ b/lib/model/filters/coordinate.dart @@ -4,8 +4,8 @@ import 'package:aves/model/settings/enums/coordinate_format.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/geo_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/model/geotiff.dart b/lib/model/geotiff.dart index f86c2a909..dc3ab8deb 100644 --- a/lib/model/geotiff.dart +++ b/lib/model/geotiff.dart @@ -5,9 +5,8 @@ import 'dart:ui' as ui; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; import 'package:aves/ref/geotiff.dart'; -import 'package:aves/utils/geo_utils.dart'; import 'package:aves/utils/math_utils.dart'; -import 'package:aves/widgets/common/map/tile.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; @@ -40,7 +39,7 @@ class GeoTiffInfo extends Equatable { } } -class MappedGeoTiff { +class MappedGeoTiff with MapOverlay { final AvesEntry entry; late LatLng? Function(Point pixel) pointToLatLng; late Point? Function(Point smPoint) epsg3857ToPoint; @@ -129,6 +128,7 @@ class MappedGeoTiff { }; } + @override Future getTile(int tx, int ty, int? zoomLevel) async { zoomLevel ??= 0; @@ -217,15 +217,24 @@ class MappedGeoTiff { ); } + @override + String get id => entry.uri; + + @override + ImageProvider get imageProvider => entry.uriImage; + int get width => entry.width; int get height => entry.height; + @override bool get canOverlay => center != null; LatLng? get center => pointToLatLng(Point((width / 2).round(), (height / 2).round())); + @override LatLng? get topLeft => pointToLatLng(const Point(0, 0)); + @override LatLng? get bottomRight => pointToLatLng(Point(width, height)); } diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index 21d5baddd..e82f5cd8f 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -8,6 +8,7 @@ import 'package:aves/model/source/enums.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; class SettingsDefaults { diff --git a/lib/model/settings/enums/coordinate_format.dart b/lib/model/settings/enums/coordinate_format.dart index 1ff17855f..4b3b2ffd9 100644 --- a/lib/model/settings/enums/coordinate_format.dart +++ b/lib/model/settings/enums/coordinate_format.dart @@ -1,5 +1,4 @@ import 'package:aves/l10n/l10n.dart'; -import 'package:aves/utils/geo_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; @@ -33,14 +32,32 @@ extension ExtraCoordinateFormat on CoordinateFormat { final locale = l10n.localeName; final lat = latLng.latitude; final lng = latLng.longitude; - final latSexa = GeoUtils.decimal2sexagesimal(lat, minuteSecondPadding, secondDecimals, locale); - final lngSexa = GeoUtils.decimal2sexagesimal(lng, minuteSecondPadding, secondDecimals, locale); + final latSexa = _decimal2sexagesimal(lat, minuteSecondPadding, secondDecimals, locale); + final lngSexa = _decimal2sexagesimal(lng, minuteSecondPadding, secondDecimals, locale); return [ l10n.coordinateDms(latSexa, lat < 0 ? l10n.coordinateDmsSouth : l10n.coordinateDmsNorth), l10n.coordinateDms(lngSexa, lng < 0 ? l10n.coordinateDmsWest : l10n.coordinateDmsEast), ]; } + static String _decimal2sexagesimal( + double degDecimal, + bool minuteSecondPadding, + int secondDecimals, + String locale, + ) { + final degAbs = degDecimal.abs(); + final deg = degAbs.toInt(); + final minDecimal = (degAbs - deg) * 60; + final min = minDecimal.toInt(); + final sec = (minDecimal - min) * 60; + + var minText = NumberFormat('0' * (minuteSecondPadding ? 2 : 1), locale).format(min); + var secText = NumberFormat('${'0' * (minuteSecondPadding ? 2 : 1)}${secondDecimals > 0 ? '.${'0' * secondDecimals}' : ''}', locale).format(sec); + + return '$deg° $minText′ $secText″'; + } + static List _toDecimal(AppLocalizations l10n, LatLng latLng) { final locale = l10n.localeName; final formatter = NumberFormat('0.000000°', locale); diff --git a/lib/model/settings/enums/enums.dart b/lib/model/settings/enums/enums.dart index 461541d2c..073555371 100644 --- a/lib/model/settings/enums/enums.dart +++ b/lib/model/settings/enums/enums.dart @@ -12,9 +12,6 @@ enum CoordinateFormat { dms, decimal } enum EntryBackground { black, white, checkered } -// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/ -enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor } - enum HomePageSetting { collection, albums } enum KeepScreenOn { never, viewerOnly, always } diff --git a/lib/model/settings/enums/map_style.dart b/lib/model/settings/enums/map_style.dart index fa5f8eedb..359436b2b 100644 --- a/lib/model/settings/enums/map_style.dart +++ b/lib/model/settings/enums/map_style.dart @@ -1,8 +1,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/widgets.dart'; -import 'enums.dart'; - extension ExtraEntryMapStyle on EntryMapStyle { String getName(BuildContext context) { switch (this) { @@ -12,6 +11,10 @@ extension ExtraEntryMapStyle on EntryMapStyle { return context.l10n.mapStyleGoogleHybrid; case EntryMapStyle.googleTerrain: return context.l10n.mapStyleGoogleTerrain; + case EntryMapStyle.hmsNormal: + return context.l10n.mapStyleHuaweiNormal; + case EntryMapStyle.hmsTerrain: + return context.l10n.mapStyleHuaweiTerrain; case EntryMapStyle.osmHot: return context.l10n.mapStyleOsmHot; case EntryMapStyle.stamenToner: @@ -21,14 +24,27 @@ extension ExtraEntryMapStyle on EntryMapStyle { } } - bool get isGoogleMaps { + bool get isHeavy { switch (this) { case EntryMapStyle.googleNormal: case EntryMapStyle.googleHybrid: case EntryMapStyle.googleTerrain: + case EntryMapStyle.hmsNormal: + case EntryMapStyle.hmsTerrain: return true; default: return false; } } + + bool get needDeviceService { + switch (this) { + case EntryMapStyle.osmHot: + case EntryMapStyle.stamenToner: + case EntryMapStyle.stamenWatercolor: + return false; + default: + return true; + } + } } diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index f3f4df159..780e24bbc 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -11,6 +11,8 @@ import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/services/accessibility_service.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves_map/aves_map.dart'; +import 'package:aves_services_platform/aves_services_platform.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -161,11 +163,11 @@ class Settings extends ChangeNotifier { enableOverlayBlurEffect = performanceClass >= 29; // availability - final canUseGoogleMaps = await availability.canUseGoogleMaps; - if (canUseGoogleMaps) { - infoMapStyle = EntryMapStyle.googleNormal; + final isDeviceMapAvailable = await availability.canUseDeviceMaps; + if (isDeviceMapAvailable) { + infoMapStyle = PlatformMobileServices().defaultMapStyle; } else { - final styles = EntryMapStyle.values.whereNot((v) => v.isGoogleMaps).toList(); + final styles = EntryMapStyle.values.whereNot((v) => v.needDeviceService).toList(); infoMapStyle = styles[Random().nextInt(styles.length)]; } diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index b84506d91..6850bf251 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -116,17 +116,6 @@ class Constants { license: 'MIT', sourceUrl: 'https://github.com/ajinasokan/flutter_displaymode', ), - Dependency( - name: 'Google API Availability', - license: 'MIT', - sourceUrl: 'https://github.com/Baseflow/flutter-google-api-availability', - ), - Dependency( - name: 'Google Maps for Flutter', - license: 'BSD 3-Clause', - licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter', - ), Dependency( name: 'Package Info Plus', license: 'BSD 3-Clause', @@ -172,7 +161,39 @@ class Constants { ), ]; + static const List _googleMobileServices = [ + Dependency( + name: 'Google API Availability', + license: 'MIT', + sourceUrl: 'https://github.com/Baseflow/flutter-google-api-availability', + ), + Dependency( + name: 'Google Maps for Flutter', + license: 'BSD 3-Clause', + licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter/LICENSE', + sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter', + ), + ]; + + static const List _huaweiMobileServices = [ + Dependency( + name: 'Huawei Mobile Services (Availability, Map)', + license: 'Apache 2.0', + licenseUrl: 'https://github.com/HMS-Core/hms-flutter-plugin/blob/master/LICENCE', + sourceUrl: 'https://github.com/HMS-Core/hms-flutter-plugin', + ), + ]; + + static const List _flutterPluginsHuaweiOnly = [ + ..._huaweiMobileServices, + ]; + + static const List _flutterPluginsIzzyOnly = [ + ..._googleMobileServices, + ]; + static const List _flutterPluginsPlayOnly = [ + ..._googleMobileServices, Dependency( name: 'FlutterFire (Core, Crashlytics)', license: 'BSD 3-Clause', @@ -182,6 +203,8 @@ class Constants { static List flutterPlugins(AppFlavor flavor) => [ ..._flutterPluginsCommon, + if (flavor == AppFlavor.huawei) ..._flutterPluginsHuaweiOnly, + if (flavor == AppFlavor.izzy) ..._flutterPluginsIzzyOnly, if (flavor == AppFlavor.play) ..._flutterPluginsPlayOnly, ]; diff --git a/lib/widgets/about/bug_report.dart b/lib/widgets/about/bug_report.dart index 20a08aaa9..68c5dfcdc 100644 --- a/lib/widgets/about/bug_report.dart +++ b/lib/widgets/about/bug_report.dart @@ -15,6 +15,7 @@ import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/buttons.dart'; +import 'package:aves_services_platform/aves_services_platform.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -159,7 +160,7 @@ class _BugReportState extends State with FeedbackMixin { final packageInfo = await PackageInfo.fromPlatform(); final androidInfo = await DeviceInfoPlugin().androidInfo; final installer = await androidAppService.getAppInstaller(); - final hasPlayServices = await availability.hasPlayServices; + final hasMobileServices = await PlatformMobileServices().isServiceAvailable(); final flavor = context.read().toString().split('.')[1]; return [ 'Aves version: ${packageInfo.version}-$flavor (Build ${packageInfo.buildNumber})', @@ -167,7 +168,7 @@ class _BugReportState extends State with FeedbackMixin { 'Android version: ${androidInfo.version.release} (SDK ${androidInfo.version.sdkInt})', 'Android build: ${androidInfo.display}', 'Device: ${androidInfo.manufacturer} ${androidInfo.model}', - 'Google Play services: ${hasPlayServices ? 'ready' : 'not available'}', + 'Mobile services: ${hasMobileServices ? 'ready' : 'not available'}', 'System locales: ${WidgetsBinding.instance!.window.locales.join(', ')}', 'Aves locale: ${settings.locale ?? 'system'} -> ${settings.appliedLocale}', 'Installer: $installer', diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 46adbc61a..7a0d5cfa5 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -30,6 +30,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; import 'package:aves/widgets/home_page.dart'; import 'package:aves/widgets/welcome_page.dart'; +import 'package:aves_services_platform/aves_services_platform.dart'; import 'package:equatable/equatable.dart'; import 'package:fijkplayer/fijkplayer.dart'; import 'package:flutter/foundation.dart'; @@ -271,14 +272,14 @@ class _AvesAppState extends State with WidgetsBindingObserver { FlutterError.onError = reportService.recordFlutterError; final now = DateTime.now(); - final hasPlayServices = await availability.hasPlayServices; + final hasMobileServices = await PlatformMobileServices().isServiceAvailable(); await reportService.setCustomKeys({ 'build_mode': kReleaseMode ? 'release' : kProfileMode ? 'profile' : 'debug', - 'has_play_services': hasPlayServices, + 'has_mobile_services': hasMobileServices, 'locales': WidgetsBinding.instance!.window.locales.join(', '), 'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})', }); diff --git a/lib/widgets/common/map/attribution.dart b/lib/widgets/common/map/attribution.dart index 9488b38cb..5f4060185 100644 --- a/lib/widgets/common/map/attribution.dart +++ b/lib/widgets/common/map/attribution.dart @@ -1,6 +1,6 @@ -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/info/common.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/lib/widgets/common/map/buttons/button.dart b/lib/widgets/common/map/buttons/button.dart new file mode 100644 index 000000000..77fdf23d0 --- /dev/null +++ b/lib/widgets/common/map/buttons/button.dart @@ -0,0 +1,55 @@ +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/themes.dart'; +import 'package:aves/widgets/common/fx/blurred.dart'; +import 'package:aves/widgets/common/fx/borders.dart'; +import 'package:aves_map/aves_map.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class MapOverlayButton extends StatelessWidget { + final Widget icon; + final String tooltip; + final VoidCallback? onPressed; + + const MapOverlayButton({ + Key? key, + required this.icon, + required this.tooltip, + required this.onPressed, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final blurred = settings.enableOverlayBlurEffect; + return Selector>( + selector: (context, v) => v.scale, + builder: (context, scale, child) => ScaleTransition( + scale: scale, + child: child, + ), + child: BlurredOval( + enabled: blurred, + child: Material( + type: MaterialType.circle, + color: Themes.overlayBackgroundColor(brightness: Theme.of(context).brightness, blurred: blurred), + child: Ink( + decoration: BoxDecoration( + border: AvesBorder.border(context), + shape: BoxShape.circle, + ), + child: Selector( + selector: (context, v) => v.visualDensity, + builder: (context, visualDensity, child) => IconButton( + iconSize: 20, + visualDensity: visualDensity, + icon: icon, + onPressed: onPressed, + tooltip: tooltip, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/common/map/buttons/coordinate_filter.dart b/lib/widgets/common/map/buttons/coordinate_filter.dart new file mode 100644 index 000000000..c9639d069 --- /dev/null +++ b/lib/widgets/common/map/buttons/coordinate_filter.dart @@ -0,0 +1,111 @@ +import 'package:aves/model/filters/coordinate.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/themes.dart'; +import 'package:aves/utils/debouncer.dart'; +import 'package:aves/widgets/common/fx/blurred.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:aves/widgets/viewer/notifications.dart'; +import 'package:aves_map/aves_map.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class OverlayCoordinateFilterChip extends StatefulWidget { + final ValueNotifier boundsNotifier; + final double padding; + + const OverlayCoordinateFilterChip({ + Key? key, + required this.boundsNotifier, + required this.padding, + }) : super(key: key); + + @override + State createState() => _OverlayCoordinateFilterChipState(); +} + +class _OverlayCoordinateFilterChipState extends State { + final Debouncer _debouncer = Debouncer(delay: Durations.mapInfoDebounceDelay); + final ValueNotifier _idleBoundsNotifier = ValueNotifier(null); + + @override + void initState() { + super.initState(); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant OverlayCoordinateFilterChip oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(OverlayCoordinateFilterChip widget) { + widget.boundsNotifier.addListener(_onBoundsChanged); + } + + void _unregisterWidget(OverlayCoordinateFilterChip widget) { + widget.boundsNotifier.removeListener(_onBoundsChanged); + } + + @override + Widget build(BuildContext context) { + final blurred = settings.enableOverlayBlurEffect; + final theme = Theme.of(context); + return Theme( + data: theme.copyWith( + scaffoldBackgroundColor: Themes.overlayBackgroundColor(brightness: theme.brightness, blurred: blurred), + ), + child: Align( + alignment: Alignment.topLeft, + child: Selector>( + selector: (context, v) => v.scale, + builder: (context, scale, child) => SizeTransition( + sizeFactor: scale, + axisAlignment: 1, + child: FadeTransition( + opacity: scale, + child: child, + ), + ), + child: ValueListenableBuilder( + valueListenable: _idleBoundsNotifier, + builder: (context, bounds, child) { + if (bounds == null) return const SizedBox(); + final filter = CoordinateFilter( + bounds.sw, + bounds.ne, + // more stable format when bounds change + minuteSecondPadding: true, + ); + return Padding( + padding: EdgeInsets.all(widget.padding), + child: BlurredRRect.all( + enabled: blurred, + borderRadius: AvesFilterChip.defaultRadius, + child: AvesFilterChip( + filter: filter, + useFilterColor: false, + maxWidth: double.infinity, + onTap: (filter) => FilterSelectedNotification(CoordinateFilter(bounds.sw, bounds.ne)).dispatch(context), + ), + ), + ); + }, + ), + ), + ), + ); + } + + void _onBoundsChanged() { + _debouncer(() => _idleBoundsNotifier.value = widget.boundsNotifier.value); + } +} diff --git a/lib/widgets/common/map/buttons.dart b/lib/widgets/common/map/buttons/panel.dart similarity index 56% rename from lib/widgets/common/map/buttons.dart rename to lib/widgets/common/map/buttons/panel.dart index 976f5561b..6608cf1bc 100644 --- a/lib/widgets/common/map/buttons.dart +++ b/lib/widgets/common/map/buttons/panel.dart @@ -1,31 +1,23 @@ -import 'package:aves/model/filters/coordinate.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/theme/themes.dart'; -import 'package:aves/utils/debouncer.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/fx/blurred.dart'; -import 'package:aves/widgets/common/fx/borders.dart'; -import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:aves/widgets/common/map/buttons/button.dart'; +import 'package:aves/widgets/common/map/buttons/coordinate_filter.dart'; import 'package:aves/widgets/common/map/compass.dart'; -import 'package:aves/widgets/common/map/theme.dart'; -import 'package:aves/widgets/common/map/zoomed_bounds.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; -import 'package:aves/widgets/viewer/notifications.dart'; +import 'package:aves_map/aves_map.dart'; +import 'package:aves_services_platform/aves_services_platform.dart'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; -typedef MapOpener = void Function(BuildContext context); - class MapButtonPanel extends StatelessWidget { final ValueNotifier boundsNotifier; final Future Function(double amount)? zoomBy; - final MapOpener? openMapPage; + final void Function(BuildContext context)? openMapPage; final VoidCallback? resetRotation; const MapButtonPanel({ @@ -123,7 +115,7 @@ class MapButtonPanel extends StatelessWidget { ), showCoordinateFilter ? Expanded( - child: _OverlayCoordinateFilterChip( + child: OverlayCoordinateFilterChip( boundsNotifier: boundsNotifier, padding: padding, ), @@ -134,8 +126,11 @@ class MapButtonPanel extends StatelessWidget { child: MapOverlayButton( icon: const Icon(AIcons.layers), onPressed: () async { - final canUseGoogleMaps = await availability.canUseGoogleMaps; - final availableStyles = EntryMapStyle.values.where((style) => !style.isGoogleMaps || canUseGoogleMaps); + final canUseDeviceMaps = await availability.canUseDeviceMaps; + final availableStyles = [ + if (canUseDeviceMaps) ...PlatformMobileServices().mapStyles, + ...EntryMapStyle.values.where((v) => !v.needDeviceService), + ]; final preferredStyle = settings.infoMapStyle; final initialStyle = availableStyles.contains(preferredStyle) ? preferredStyle : availableStyles.first; await showSelectionDialog( @@ -181,151 +176,3 @@ class MapButtonPanel extends StatelessWidget { ); } } - -class MapOverlayButton extends StatelessWidget { - final Widget icon; - final String tooltip; - final VoidCallback? onPressed; - - const MapOverlayButton({ - Key? key, - required this.icon, - required this.tooltip, - required this.onPressed, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final blurred = settings.enableOverlayBlurEffect; - return Selector>( - selector: (context, v) => v.scale, - builder: (context, scale, child) => ScaleTransition( - scale: scale, - child: child, - ), - child: BlurredOval( - enabled: blurred, - child: Material( - type: MaterialType.circle, - color: Themes.overlayBackgroundColor(brightness: Theme.of(context).brightness, blurred: blurred), - child: Ink( - decoration: BoxDecoration( - border: AvesBorder.border(context), - shape: BoxShape.circle, - ), - child: Selector( - selector: (context, v) => v.visualDensity, - builder: (context, visualDensity, child) => IconButton( - iconSize: 20, - visualDensity: visualDensity, - icon: icon, - onPressed: onPressed, - tooltip: tooltip, - ), - ), - ), - ), - ), - ); - } -} - -class _OverlayCoordinateFilterChip extends StatefulWidget { - final ValueNotifier boundsNotifier; - final double padding; - - const _OverlayCoordinateFilterChip({ - Key? key, - required this.boundsNotifier, - required this.padding, - }) : super(key: key); - - @override - State<_OverlayCoordinateFilterChip> createState() => _OverlayCoordinateFilterChipState(); -} - -class _OverlayCoordinateFilterChipState extends State<_OverlayCoordinateFilterChip> { - final Debouncer _debouncer = Debouncer(delay: Durations.mapInfoDebounceDelay); - final ValueNotifier _idleBoundsNotifier = ValueNotifier(null); - - @override - void initState() { - super.initState(); - _registerWidget(widget); - } - - @override - void didUpdateWidget(covariant _OverlayCoordinateFilterChip oldWidget) { - super.didUpdateWidget(oldWidget); - _unregisterWidget(oldWidget); - _registerWidget(widget); - } - - @override - void dispose() { - _unregisterWidget(widget); - super.dispose(); - } - - void _registerWidget(_OverlayCoordinateFilterChip widget) { - widget.boundsNotifier.addListener(_onBoundsChanged); - } - - void _unregisterWidget(_OverlayCoordinateFilterChip widget) { - widget.boundsNotifier.removeListener(_onBoundsChanged); - } - - @override - Widget build(BuildContext context) { - final blurred = settings.enableOverlayBlurEffect; - final theme = Theme.of(context); - return Theme( - data: theme.copyWith( - scaffoldBackgroundColor: Themes.overlayBackgroundColor(brightness: theme.brightness, blurred: blurred), - ), - child: Align( - alignment: Alignment.topLeft, - child: Selector>( - selector: (context, v) => v.scale, - builder: (context, scale, child) => SizeTransition( - sizeFactor: scale, - axisAlignment: 1, - child: FadeTransition( - opacity: scale, - child: child, - ), - ), - child: ValueListenableBuilder( - valueListenable: _idleBoundsNotifier, - builder: (context, bounds, child) { - if (bounds == null) return const SizedBox(); - final filter = CoordinateFilter( - bounds.sw, - bounds.ne, - // more stable format when bounds change - minuteSecondPadding: true, - ); - return Padding( - padding: EdgeInsets.all(widget.padding), - child: BlurredRRect.all( - enabled: blurred, - borderRadius: AvesFilterChip.defaultRadius, - child: AvesFilterChip( - filter: filter, - useFilterColor: false, - maxWidth: double.infinity, - onTap: (filter) => FilterSelectedNotification(CoordinateFilter(bounds.sw, bounds.ne)).dispatch(context), - ), - ), - ); - }, - ), - ), - ), - ); - } - - void _onBoundsChanged() { - _debouncer(() => _idleBoundsNotifier.value = widget.boundsNotifier.value); - } -} diff --git a/lib/widgets/common/map/decorator.dart b/lib/widgets/common/map/decorator.dart index cafee9b0e..d3ddd0ce3 100644 --- a/lib/widgets/common/map/decorator.dart +++ b/lib/widgets/common/map/decorator.dart @@ -1,5 +1,5 @@ import 'package:aves/widgets/common/fx/borders.dart'; -import 'package:aves/widgets/common/map/theme.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/common/map/geo_map.dart b/lib/widgets/common/map/geo_map.dart index 8eeb720bc..d881c8579 100644 --- a/lib/widgets/common/map/geo_map.dart +++ b/lib/widgets/common/map/geo_map.dart @@ -2,8 +2,7 @@ import 'dart:async'; import 'dart:math'; import 'package:aves/model/entry.dart'; -import 'package:aves/model/geotiff.dart'; -import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/entry_images.dart'; import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; @@ -11,17 +10,13 @@ import 'package:aves/utils/change_notifier.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/utils/math_utils.dart'; import 'package:aves/widgets/common/map/attribution.dart'; -import 'package:aves/widgets/common/map/buttons.dart'; -import 'package:aves/widgets/common/map/controller.dart'; +import 'package:aves/widgets/common/map/buttons/panel.dart'; import 'package:aves/widgets/common/map/decorator.dart'; -import 'package:aves/widgets/common/map/geo_entry.dart'; -import 'package:aves/widgets/common/map/google/map.dart'; import 'package:aves/widgets/common/map/leaflet/map.dart'; -import 'package:aves/widgets/common/map/marker.dart'; -import 'package:aves/widgets/common/map/theme.dart'; -import 'package:aves/widgets/common/map/zoomed_bounds.dart'; +import 'package:aves/widgets/common/thumbnail/image.dart'; +import 'package:aves_map/aves_map.dart'; +import 'package:aves_services_platform/aves_services_platform.dart'; import 'package:collection/collection.dart'; -import 'package:equatable/equatable.dart'; import 'package:fluster/fluster.dart'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; @@ -35,14 +30,11 @@ class GeoMap extends StatefulWidget { final ValueNotifier isAnimatingNotifier; final ValueNotifier? dotLocationNotifier; final ValueNotifier? overlayOpacityNotifier; - final MappedGeoTiff? overlayEntry; + final MapOverlay? overlayEntry; final UserZoomChangeCallback? onUserZoomChange; - final void Function(LatLng location)? onMapTap; - final MarkerTapCallback? onMarkerTap; - final MapOpener? openMapPage; - - static const markerImageExtent = 48.0; - static const markerArrowSize = Size(8, 6); + final MapTapCallback? onMapTap; + final void Function(LatLng averageLocation, AvesEntry markerEntry, Set Function() getClusterEntries)? onMarkerTap; + final void Function(BuildContext context)? openMapPage; const GeoMap({ Key? key, @@ -71,14 +63,16 @@ class _GeoMapState extends State { // cf https://github.com/flutter/flutter/issues/28493 // it is especially severe the first time, but still significant afterwards // so we prevent loading it while scrolling or animating - bool _googleMapsLoaded = false; + bool _heavyMapLoaded = false; late final ValueNotifier _boundsNotifier; - Fluster? _defaultMarkerCluster; - Fluster? _slowMarkerCluster; + Fluster>? _defaultMarkerCluster; + Fluster>? _slowMarkerCluster; final AChangeNotifier _clusterChangeNotifier = AChangeNotifier(); List get entries => widget.entries; + static final _platformMobileServices = PlatformMobileServices(); + // cap initial zoom to avoid a zoom change // when toggling overlay on Google map initial state static const double minInitialZoom = 3; @@ -121,7 +115,7 @@ class _GeoMapState extends State { @override Widget build(BuildContext context) { - void _onMarkerTap(GeoEntry geoEntry) { + void _onMarkerTap(GeoEntry geoEntry) { final onTap = widget.onMarkerTap; if (onTap == null) return; @@ -159,60 +153,74 @@ class _GeoMapState extends State { return Selector( selector: (context, s) => s.infoMapStyle, builder: (context, mapStyle, child) { - final isGoogleMaps = mapStyle.isGoogleMaps; - final progressive = !isGoogleMaps; - Widget _buildMarkerWidget(MarkerKey key) => ImageMarker( + final isHeavy = mapStyle.isHeavy; + Widget _buildMarkerWidget(MarkerKey key) => ImageMarker( key: key, - entry: key.entry, count: key.count, - extent: GeoMap.markerImageExtent, - arrowSize: GeoMap.markerArrowSize, - progressive: progressive, + buildThumbnailImage: (extent) => ThumbnailImage( + entry: key.entry, + extent: extent, + progressive: !isHeavy, + ), ); + bool _isMarkerImageReady(MarkerKey key) => key.entry.isThumbnailReady(extent: MapThemeData.markerImageExtent); - Widget child = isGoogleMaps - ? EntryGoogleMap( - controller: widget.controller, - clusterListenable: _clusterChangeNotifier, - boundsNotifier: _boundsNotifier, - minZoom: 0, - maxZoom: 20, - style: mapStyle, - markerClusterBuilder: _buildMarkerClusters, - markerWidgetBuilder: _buildMarkerWidget, - dotLocationNotifier: widget.dotLocationNotifier, - overlayOpacityNotifier: widget.overlayOpacityNotifier, - overlayEntry: widget.overlayEntry, - onUserZoomChange: widget.onUserZoomChange, - onMapTap: widget.onMapTap, - onMarkerTap: _onMarkerTap, - openMapPage: widget.openMapPage, - ) - : EntryLeafletMap( - controller: widget.controller, - clusterListenable: _clusterChangeNotifier, - boundsNotifier: _boundsNotifier, - minZoom: 2, - maxZoom: 16, - style: mapStyle, - markerClusterBuilder: _buildMarkerClusters, - markerWidgetBuilder: _buildMarkerWidget, - dotLocationNotifier: widget.dotLocationNotifier, - markerSize: Size( - GeoMap.markerImageExtent + ImageMarker.outerBorderWidth * 2, - GeoMap.markerImageExtent + ImageMarker.outerBorderWidth * 2 + GeoMap.markerArrowSize.height, - ), - dotMarkerSize: const Size( - DotMarker.diameter + ImageMarker.outerBorderWidth * 2, - DotMarker.diameter + ImageMarker.outerBorderWidth * 2, - ), - overlayOpacityNotifier: widget.overlayOpacityNotifier, - overlayEntry: widget.overlayEntry, - onUserZoomChange: widget.onUserZoomChange, - onMapTap: widget.onMapTap, - onMarkerTap: _onMarkerTap, - openMapPage: widget.openMapPage, - ); + Widget child = const SizedBox(); + switch (mapStyle) { + case EntryMapStyle.googleNormal: + case EntryMapStyle.googleHybrid: + case EntryMapStyle.googleTerrain: + case EntryMapStyle.hmsNormal: + case EntryMapStyle.hmsTerrain: + child = _platformMobileServices.buildMap( + controller: widget.controller, + clusterListenable: _clusterChangeNotifier, + boundsNotifier: _boundsNotifier, + style: mapStyle, + decoratorBuilder: _decorateMap, + buttonPanelBuilder: _buildButtonPanel, + markerClusterBuilder: _buildMarkerClusters, + markerWidgetBuilder: _buildMarkerWidget, + markerImageReadyChecker: _isMarkerImageReady, + dotLocationNotifier: widget.dotLocationNotifier, + overlayOpacityNotifier: widget.overlayOpacityNotifier, + overlayEntry: widget.overlayEntry, + onUserZoomChange: widget.onUserZoomChange, + onMapTap: widget.onMapTap, + onMarkerTap: _onMarkerTap, + ); + break; + case EntryMapStyle.osmHot: + case EntryMapStyle.stamenToner: + case EntryMapStyle.stamenWatercolor: + child = EntryLeafletMap( + controller: widget.controller, + clusterListenable: _clusterChangeNotifier, + boundsNotifier: _boundsNotifier, + minZoom: 2, + maxZoom: 16, + style: mapStyle, + decoratorBuilder: _decorateMap, + buttonPanelBuilder: _buildButtonPanel, + markerClusterBuilder: _buildMarkerClusters, + markerWidgetBuilder: _buildMarkerWidget, + dotLocationNotifier: widget.dotLocationNotifier, + markerSize: Size( + MapThemeData.markerImageExtent + MapThemeData.markerOuterBorderWidth * 2, + MapThemeData.markerImageExtent + MapThemeData.markerOuterBorderWidth * 2 + MapThemeData.markerArrowSize.height, + ), + dotMarkerSize: const Size( + DotMarker.diameter + MapThemeData.markerOuterBorderWidth * 2, + DotMarker.diameter + MapThemeData.markerOuterBorderWidth * 2, + ), + overlayOpacityNotifier: widget.overlayOpacityNotifier, + overlayEntry: widget.overlayEntry, + onUserZoomChange: widget.onUserZoomChange, + onMapTap: widget.onMapTap, + onMarkerTap: _onMarkerTap, + ); + break; + } final mapHeight = context.select((v) => v.mapHeight); child = Column( @@ -239,8 +247,8 @@ class _GeoMapState extends State { child: ValueListenableBuilder( valueListenable: widget.isAnimatingNotifier, builder: (context, animating, child) { - if (!animating && isGoogleMaps) { - _googleMapsLoaded = true; + if (!animating && isHeavy) { + _heavyMapLoaded = true; } Widget replacement = Stack( children: [ @@ -258,7 +266,7 @@ class _GeoMapState extends State { ); } return Visibility( - visible: !isGoogleMaps || _googleMapsLoaded, + visible: !isHeavy || _heavyMapLoaded, replacement: replacement, child: child!, ); @@ -303,10 +311,10 @@ class _GeoMapState extends State { _clusterChangeNotifier.notify(); } - Fluster _buildFluster({int nodeSize = 64}) { + Fluster> _buildFluster({int nodeSize = 64}) { final markers = entries.map((entry) { final latLng = entry.latLng!; - return GeoEntry( + return GeoEntry( entry: entry, latitude: latLng.latitude, longitude: latLng.longitude, @@ -314,7 +322,7 @@ class _GeoMapState extends State { ); }).toList(); - return Fluster( + return Fluster>( // we keep clustering on the whole range of zooms (including the maximum) // to avoid collocated entries overlapping minZoom: 0, @@ -329,11 +337,11 @@ class _GeoMapState extends State { // use lambda instead of tear-off because of runtime exception when using // `T Function(BaseCluster, double, double)` for `T Function(BaseCluster?, double?, double?)` // ignore: unnecessary_lambdas - createCluster: (base, lng, lat) => GeoEntry.createCluster(base, lng, lat), + createCluster: (base, lng, lat) => GeoEntry.createCluster(base, lng, lat), ); } - Map _buildMarkerClusters() { + Map, GeoEntry> _buildMarkerClusters() { final bounds = _boundsNotifier.value; final geoEntries = _defaultMarkerCluster?.clusters(bounds.boundingBox, bounds.zoom.round()) ?? []; return Map.fromEntries(geoEntries.map((v) { @@ -345,20 +353,17 @@ class _GeoMapState extends State { return MapEntry(MarkerKey(v.entry!, null), v); })); } + + Widget _decorateMap(BuildContext context, Widget? child) => MapDecorator(child: child); + + Widget _buildButtonPanel( + Future Function(double amount) zoomBy, + VoidCallback resetRotation, + ) => + MapButtonPanel( + boundsNotifier: _boundsNotifier, + zoomBy: zoomBy, + openMapPage: widget.openMapPage, + resetRotation: resetRotation, + ); } - -@immutable -class MarkerKey extends LocalKey with EquatableMixin { - final AvesEntry entry; - final int? count; - - @override - List get props => [entry, count]; - - const MarkerKey(this.entry, this.count); -} - -typedef MarkerClusterBuilder = Map Function(); -typedef MarkerWidgetBuilder = Widget Function(MarkerKey key); -typedef UserZoomChangeCallback = void Function(double zoom); -typedef MarkerTapCallback = void Function(LatLng averageLocation, AvesEntry markerEntry, Set Function() getClusterEntries); diff --git a/lib/widgets/common/map/google/geotiff_tile_provider.dart b/lib/widgets/common/map/google/geotiff_tile_provider.dart deleted file mode 100644 index 72d5ccb03..000000000 --- a/lib/widgets/common/map/google/geotiff_tile_provider.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:aves/model/geotiff.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; - -class GeoTiffTileProvider extends TileProvider { - MappedGeoTiff overlayEntry; - - GeoTiffTileProvider(this.overlayEntry); - - @override - Future getTile(int x, int y, int? zoom) async { - final tile = await overlayEntry.getTile(x, y, zoom); - if (tile != null) { - return Tile(tile.width, tile.height, tile.data); - } - return TileProvider.noTile; - } -} diff --git a/lib/widgets/common/map/latlng_tween.dart b/lib/widgets/common/map/leaflet/latlng_tween.dart similarity index 83% rename from lib/widgets/common/map/latlng_tween.dart rename to lib/widgets/common/map/leaflet/latlng_tween.dart index 912e14179..2b3d6960c 100644 --- a/lib/widgets/common/map/latlng_tween.dart +++ b/lib/widgets/common/map/leaflet/latlng_tween.dart @@ -1,4 +1,4 @@ -import 'package:aves/widgets/common/map/latlng_utils.dart'; +import 'package:aves/widgets/common/map/leaflet/latlng_utils.dart'; import 'package:flutter/widgets.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/widgets/common/map/latlng_utils.dart b/lib/widgets/common/map/leaflet/latlng_utils.dart similarity index 100% rename from lib/widgets/common/map/latlng_utils.dart rename to lib/widgets/common/map/leaflet/latlng_utils.dart diff --git a/lib/widgets/common/map/leaflet/map.dart b/lib/widgets/common/map/leaflet/map.dart index 6618cb87f..b66ea6ae1 100644 --- a/lib/widgets/common/map/leaflet/map.dart +++ b/lib/widgets/common/map/leaflet/map.dart @@ -1,43 +1,34 @@ import 'dart:async'; -import 'package:aves/model/entry_images.dart'; -import 'package:aves/model/geotiff.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/debouncer.dart'; -import 'package:aves/widgets/common/map/buttons.dart'; -import 'package:aves/widgets/common/map/controller.dart'; -import 'package:aves/widgets/common/map/decorator.dart'; -import 'package:aves/widgets/common/map/geo_entry.dart'; -import 'package:aves/widgets/common/map/geo_map.dart'; -import 'package:aves/widgets/common/map/latlng_tween.dart'; +import 'package:aves/widgets/common/map/leaflet/latlng_tween.dart'; import 'package:aves/widgets/common/map/leaflet/scale_layer.dart'; import 'package:aves/widgets/common/map/leaflet/tile_layers.dart'; -import 'package:aves/widgets/common/map/marker.dart'; -import 'package:aves/widgets/common/map/theme.dart'; -import 'package:aves/widgets/common/map/zoomed_bounds.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; -class EntryLeafletMap extends StatefulWidget { +class EntryLeafletMap extends StatefulWidget { final AvesMapController? controller; final Listenable clusterListenable; final ValueNotifier boundsNotifier; final double minZoom, maxZoom; final EntryMapStyle style; - final MarkerClusterBuilder markerClusterBuilder; - final MarkerWidgetBuilder markerWidgetBuilder; + final TransitionBuilder decoratorBuilder; + final ButtonPanelBuilder buttonPanelBuilder; + final MarkerClusterBuilder markerClusterBuilder; + final MarkerWidgetBuilder markerWidgetBuilder; final ValueNotifier? dotLocationNotifier; final Size markerSize, dotMarkerSize; final ValueNotifier? overlayOpacityNotifier; - final MappedGeoTiff? overlayEntry; + final MapOverlay? overlayEntry; final UserZoomChangeCallback? onUserZoomChange; - final void Function(LatLng location)? onMapTap; - final void Function(GeoEntry geoEntry)? onMarkerTap; - final MapOpener? openMapPage; + final MapTapCallback? onMapTap; + final MarkerTapCallback? onMarkerTap; const EntryLeafletMap({ Key? key, @@ -47,6 +38,8 @@ class EntryLeafletMap extends StatefulWidget { this.minZoom = 0, this.maxZoom = 22, required this.style, + required this.decoratorBuilder, + required this.buttonPanelBuilder, required this.markerClusterBuilder, required this.markerWidgetBuilder, required this.dotLocationNotifier, @@ -57,17 +50,16 @@ class EntryLeafletMap extends StatefulWidget { this.onUserZoomChange, this.onMapTap, this.onMarkerTap, - this.openMapPage, }) : super(key: key); @override - State createState() => _EntryLeafletMapState(); + State createState() => _EntryLeafletMapState(); } -class _EntryLeafletMapState extends State with TickerProviderStateMixin { +class _EntryLeafletMapState extends State> with TickerProviderStateMixin { final MapController _leafletMapController = MapController(); final List _subscriptions = []; - Map _geoEntryByMarkerKey = {}; + Map, GeoEntry> _geoEntryByMarkerKey = {}; final Debouncer _debouncer = Debouncer(delay: Durations.mapIdleDebounceDelay); ValueNotifier get boundsNotifier => widget.boundsNotifier; @@ -85,7 +77,7 @@ class _EntryLeafletMapState extends State with TickerProviderSt } @override - void didUpdateWidget(covariant EntryLeafletMap oldWidget) { + void didUpdateWidget(covariant EntryLeafletMap oldWidget) { super.didUpdateWidget(oldWidget); _unregisterWidget(oldWidget); _registerWidget(widget); @@ -97,7 +89,7 @@ class _EntryLeafletMapState extends State with TickerProviderSt super.dispose(); } - void _registerWidget(EntryLeafletMap widget) { + void _registerWidget(EntryLeafletMap widget) { final avesMapController = widget.controller; if (avesMapController != null) { _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(event.latLng))); @@ -107,7 +99,7 @@ class _EntryLeafletMapState extends State with TickerProviderSt widget.boundsNotifier.addListener(_onBoundsChange); } - void _unregisterWidget(EntryLeafletMap widget) { + void _unregisterWidget(EntryLeafletMap widget) { widget.clusterListenable.removeListener(_updateMarkers); widget.boundsNotifier.removeListener(_onBoundsChange); _subscriptions @@ -119,15 +111,8 @@ class _EntryLeafletMapState extends State with TickerProviderSt Widget build(BuildContext context) { return Stack( children: [ - MapDecorator( - child: _buildMap(), - ), - MapButtonPanel( - boundsNotifier: boundsNotifier, - zoomBy: _zoomBy, - openMapPage: widget.openMapPage, - resetRotation: _resetRotation, - ), + widget.decoratorBuilder(context, _buildMap()), + widget.buttonPanelBuilder(_zoomBy, _resetRotation), ], ); } @@ -239,7 +224,7 @@ class _EntryLeafletMapState extends State with TickerProviderSt overlayImages: [ OverlayImage( bounds: LatLngBounds(corner1, corner2), - imageProvider: overlayEntry.entry.uriImage, + imageProvider: overlayEntry.imageProvider, opacity: overlayOpacity, ), ], diff --git a/lib/widgets/common/map/theme.dart b/lib/widgets/common/providers/map_theme_provider.dart similarity index 70% rename from lib/widgets/common/map/theme.dart rename to lib/widgets/common/providers/map_theme_provider.dart index b0e70ee40..c4856fb51 100644 --- a/lib/widgets/common/map/theme.dart +++ b/lib/widgets/common/providers/map_theme_provider.dart @@ -1,9 +1,8 @@ import 'package:aves/model/settings/settings.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -enum MapNavigationButton { back, map } - class MapTheme extends StatelessWidget { final bool interactive, showCoordinateFilter; final MapNavigationButton navigationButton; @@ -40,20 +39,3 @@ class MapTheme extends StatelessWidget { ); } } - -class MapThemeData { - final bool interactive, showCoordinateFilter; - final MapNavigationButton navigationButton; - final Animation scale; - final VisualDensity? visualDensity; - final double? mapHeight; - - const MapThemeData({ - required this.interactive, - required this.showCoordinateFilter, - required this.navigationButton, - required this.scale, - required this.visualDensity, - required this.mapHeight, - }); -} diff --git a/lib/widgets/dialogs/location_pick_dialog.dart b/lib/widgets/dialogs/location_pick_dialog.dart index 00b3ede5d..a0d446753 100644 --- a/lib/widgets/dialogs/location_pick_dialog.dart +++ b/lib/widgets/dialogs/location_pick_dialog.dart @@ -12,12 +12,10 @@ import 'package:aves/utils/constants.dart'; import 'package:aves/utils/debouncer.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/buttons.dart'; -import 'package:aves/widgets/common/map/controller.dart'; import 'package:aves/widgets/common/map/geo_map.dart'; -import 'package:aves/widgets/common/map/marker.dart'; -import 'package:aves/widgets/common/map/theme.dart'; -import 'package:aves/widgets/common/map/zoomed_bounds.dart'; +import 'package:aves/widgets/common/providers/map_theme_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -82,7 +80,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin void initState() { super.initState(); - if (settings.infoMapStyle.isGoogleMaps) { + if (settings.infoMapStyle.isHeavy) { _isPageAnimatingNotifier = ValueNotifier(true); Future.delayed(Durations.pageTransitionAnimation * timeDilation).then((_) { if (!mounted) return; diff --git a/lib/widgets/map/map_info_row.dart b/lib/widgets/map/map_info_row.dart index 9670d26b9..1dd83b1d9 100644 --- a/lib/widgets/map/map_info_row.dart +++ b/lib/widgets/map/map_info_row.dart @@ -7,7 +7,7 @@ import 'package:aves/theme/format.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/map/marker.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index 000557b94..cec4cd9da 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -6,7 +6,6 @@ import 'package:aves/model/filters/coordinate.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/geotiff.dart'; import 'package:aves/model/highlight.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; @@ -17,16 +16,15 @@ import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/empty.dart'; -import 'package:aves/widgets/common/map/controller.dart'; import 'package:aves/widgets/common/map/geo_map.dart'; -import 'package:aves/widgets/common/map/theme.dart'; -import 'package:aves/widgets/common/map/zoomed_bounds.dart'; import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; +import 'package:aves/widgets/common/providers/map_theme_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/thumbnail/scroller.dart'; import 'package:aves/widgets/map/map_info_row.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/notifications.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -110,7 +108,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin void initState() { super.initState(); - if (settings.infoMapStyle.isGoogleMaps) { + if (settings.infoMapStyle.isHeavy) { _isPageAnimatingNotifier.value = true; Future.delayed(Durations.pageTransitionAnimation * timeDilation).then((_) { if (!mounted) return; @@ -176,8 +174,8 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin selector: (context, s) => s.infoMapStyle, builder: (context, mapStyle, child) { late Widget scroller; - if (mapStyle.isGoogleMaps) { - // the Google map widget is too heavy for a smooth resizing animation + if (mapStyle.isHeavy) { + // the map widget is too heavy for a smooth resizing animation // so we just toggle visibility when overlay animation is done scroller = ValueListenableBuilder( valueListenable: _overlayAnimationController, @@ -190,7 +188,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin child: child, ); } else { - // the Leaflet map widget is light enough for a smooth resizing animation + // the map widget is light enough for a smooth resizing animation scroller = FadeTransition( opacity: _scrollerSize, child: SizeTransition( diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index b71e1561f..9cc06ccdc 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -7,11 +7,11 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; -import 'package:aves/widgets/common/map/controller.dart'; import 'package:aves/widgets/common/map/geo_map.dart'; -import 'package:aves/widgets/common/map/theme.dart'; +import 'package:aves/widgets/common/providers/map_theme_provider.dart'; import 'package:aves/widgets/map/map_page.dart'; import 'package:aves/widgets/viewer/info/common.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; class LocationSection extends StatefulWidget { diff --git a/plugins/aves_map/.gitignore b/plugins/aves_map/.gitignore new file mode 100644 index 000000000..9be145fde --- /dev/null +++ b/plugins/aves_map/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/plugins/aves_map/.metadata b/plugins/aves_map/.metadata new file mode 100644 index 000000000..c24d00d29 --- /dev/null +++ b/plugins/aves_map/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 5464c5bac742001448fe4fc0597be939379f88ea + channel: stable + +project_type: package diff --git a/plugins/aves_map/analysis_options.yaml b/plugins/aves_map/analysis_options.yaml new file mode 100644 index 000000000..f04c6cf0f --- /dev/null +++ b/plugins/aves_map/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/plugins/aves_map/lib/aves_map.dart b/plugins/aves_map/lib/aves_map.dart new file mode 100644 index 000000000..3de25590d --- /dev/null +++ b/plugins/aves_map/lib/aves_map.dart @@ -0,0 +1,15 @@ +library aves_map; + +export 'src/controller.dart'; +export 'src/geo_entry.dart'; +export 'src/geo_utils.dart'; +export 'src/interface.dart'; +export 'src/marker/dot.dart'; +export 'src/marker/generator.dart'; +export 'src/marker/image.dart'; +export 'src/marker/key.dart'; +export 'src/overlay/overlay.dart'; +export 'src/overlay/tile.dart'; +export 'src/style.dart'; +export 'src/theme.dart'; +export 'src/zoomed_bounds.dart'; diff --git a/lib/widgets/common/map/controller.dart b/plugins/aves_map/lib/src/controller.dart similarity index 95% rename from lib/widgets/common/map/controller.dart rename to plugins/aves_map/lib/src/controller.dart index 4bb280bb2..e196bcf2e 100644 --- a/lib/widgets/common/map/controller.dart +++ b/plugins/aves_map/lib/src/controller.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:aves/widgets/common/map/zoomed_bounds.dart'; +import 'package:aves_map/src/zoomed_bounds.dart'; import 'package:latlong2/latlong.dart'; class AvesMapController { diff --git a/lib/widgets/common/map/geo_entry.dart b/plugins/aves_map/lib/src/geo_entry.dart similarity index 92% rename from lib/widgets/common/map/geo_entry.dart rename to plugins/aves_map/lib/src/geo_entry.dart index 9eab3b881..046ce4c1b 100644 --- a/lib/widgets/common/map/geo_entry.dart +++ b/plugins/aves_map/lib/src/geo_entry.dart @@ -1,9 +1,8 @@ -import 'package:aves/model/entry.dart'; import 'package:fluster/fluster.dart'; import 'package:flutter/foundation.dart'; -class GeoEntry extends Clusterable { - AvesEntry? entry; +class GeoEntry extends Clusterable { + T? entry; GeoEntry({ this.entry, diff --git a/lib/utils/geo_utils.dart b/plugins/aves_map/lib/src/geo_utils.dart similarity index 89% rename from lib/utils/geo_utils.dart rename to plugins/aves_map/lib/src/geo_utils.dart index a82a1ff2e..5272482f5 100644 --- a/lib/utils/geo_utils.dart +++ b/plugins/aves_map/lib/src/geo_utils.dart @@ -1,27 +1,8 @@ import 'dart:math'; -import 'package:intl/intl.dart'; import 'package:latlong2/latlong.dart'; class GeoUtils { - static String decimal2sexagesimal( - double degDecimal, - bool minuteSecondPadding, - int secondDecimals, - String locale, - ) { - final degAbs = degDecimal.abs(); - final deg = degAbs.toInt(); - final minDecimal = (degAbs - deg) * 60; - final min = minDecimal.toInt(); - final sec = (minDecimal - min) * 60; - - var minText = NumberFormat('0' * (minuteSecondPadding ? 2 : 1), locale).format(min); - var secText = NumberFormat('${'0' * (minuteSecondPadding ? 2 : 1)}${secondDecimals > 0 ? '.${'0' * secondDecimals}' : ''}', locale).format(sec); - - return '$deg° $minText′ $secText″'; - } - static LatLng getLatLngCenter(List points) { double x = 0; double y = 0; diff --git a/plugins/aves_map/lib/src/interface.dart b/plugins/aves_map/lib/src/interface.dart new file mode 100644 index 000000000..f2c31eb02 --- /dev/null +++ b/plugins/aves_map/lib/src/interface.dart @@ -0,0 +1,12 @@ +import 'package:aves_map/src/geo_entry.dart'; +import 'package:aves_map/src/marker/key.dart'; +import 'package:flutter/material.dart'; +import 'package:latlong2/latlong.dart'; + +typedef ButtonPanelBuilder = Widget Function(Future Function(double amount) zoomBy, VoidCallback resetRotation); +typedef MarkerClusterBuilder = Map, GeoEntry> Function(); +typedef MarkerWidgetBuilder = Widget Function(MarkerKey key); +typedef MarkerImageReadyChecker = bool Function(MarkerKey key); +typedef UserZoomChangeCallback = void Function(double zoom); +typedef MapTapCallback = void Function(LatLng location); +typedef MarkerTapCallback = void Function(GeoEntry geoEntry); diff --git a/plugins/aves_map/lib/src/marker/dot.dart b/plugins/aves_map/lib/src/marker/dot.dart new file mode 100644 index 000000000..ba3cc65b8 --- /dev/null +++ b/plugins/aves_map/lib/src/marker/dot.dart @@ -0,0 +1,54 @@ +import 'package:aves_map/src/theme.dart'; +import 'package:flutter/material.dart'; + +class DotMarker extends StatelessWidget { + const DotMarker({Key? key}) : super(key: key); + + static const double diameter = 16; + static const double outerBorderRadiusDim = diameter; + static const double outerBorderWidth = MapThemeData.markerOuterBorderWidth; + static const double innerBorderWidth = MapThemeData.markerInnerBorderWidth; + static const outerBorderRadius = BorderRadius.all(Radius.circular(outerBorderRadiusDim)); + static const innerRadius = Radius.circular(outerBorderRadiusDim - outerBorderWidth); + static const innerBorderRadius = BorderRadius.all(innerRadius); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final outerBorderColor = MapThemeData.markerThemedOuterBorderColor(isDark); + final innerBorderColor = MapThemeData.markerThemedInnerBorderColor(isDark); + + final outerDecoration = BoxDecoration( + border: Border.fromBorderSide(BorderSide( + color: outerBorderColor, + width: outerBorderWidth, + )), + borderRadius: outerBorderRadius, + ); + + final innerDecoration = BoxDecoration( + border: Border.fromBorderSide(BorderSide( + color: innerBorderColor, + width: innerBorderWidth, + )), + borderRadius: innerBorderRadius, + ); + + return Container( + decoration: outerDecoration, + child: DecoratedBox( + decoration: innerDecoration, + position: DecorationPosition.foreground, + child: ClipRRect( + borderRadius: innerBorderRadius, + child: Container( + width: diameter, + height: diameter, + color: theme.colorScheme.secondary, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/common/map/google/marker_generator.dart b/plugins/aves_map/lib/src/marker/generator.dart similarity index 100% rename from lib/widgets/common/map/google/marker_generator.dart rename to plugins/aves_map/lib/src/marker/generator.dart diff --git a/lib/widgets/common/map/marker.dart b/plugins/aves_map/lib/src/marker/image.dart similarity index 65% rename from lib/widgets/common/map/marker.dart rename to plugins/aves_map/lib/src/marker/image.dart index ddbfb9cad..a45d22591 100644 --- a/lib/widgets/common/map/marker.dart +++ b/plugins/aves_map/lib/src/marker/image.dart @@ -1,45 +1,29 @@ -import 'package:aves/model/entry.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/thumbnail/image.dart'; +import 'package:aves_map/src/theme.dart'; import 'package:custom_rounded_rectangle_border/custom_rounded_rectangle_border.dart'; import 'package:flutter/material.dart'; class ImageMarker extends StatelessWidget { - final AvesEntry? entry; final int? count; - final double extent; - final Size arrowSize; - final bool progressive; + final Widget Function(double extent) buildThumbnailImage; static const double outerBorderRadiusDim = 8; - static const double outerBorderWidth = 1.5; - static const double innerBorderWidth = 2; + static const outerBorderWidth = MapThemeData.markerOuterBorderWidth; + static const innerBorderWidth = MapThemeData.markerInnerBorderWidth; + static const extent = MapThemeData.markerImageExtent; + static const arrowSize = MapThemeData.markerArrowSize; static const outerBorderRadius = BorderRadius.all(Radius.circular(outerBorderRadiusDim)); static const innerRadius = Radius.circular(outerBorderRadiusDim - outerBorderWidth); static const innerBorderRadius = BorderRadius.all(innerRadius); - static Color themedOuterBorderColor(bool isDark) => isDark ? Colors.white30 : Colors.black26; - - static Color themedInnerBorderColor(bool isDark) => isDark ? const Color(0xFF212121) : Colors.white; - const ImageMarker({ Key? key, - required this.entry, required this.count, - required this.extent, - required this.arrowSize, - required this.progressive, + required this.buildThumbnailImage, }) : super(key: key); @override Widget build(BuildContext context) { - Widget child = entry != null - ? ThumbnailImage( - entry: entry!, - extent: extent, - progressive: progressive, - ) - : const SizedBox(); + Widget child = buildThumbnailImage(extent); // need to be sized for the Google map marker generator child = SizedBox( @@ -50,8 +34,8 @@ class ImageMarker extends StatelessWidget { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; - final outerBorderColor = themedOuterBorderColor(isDark); - final innerBorderColor = themedInnerBorderColor(isDark); + final outerBorderColor = MapThemeData.markerThemedOuterBorderColor(isDark); + final innerBorderColor = MapThemeData.markerThemedInnerBorderColor(isDark); final outerDecoration = BoxDecoration( border: Border.fromBorderSide(BorderSide( @@ -90,7 +74,7 @@ class ImageMarker extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 2), decoration: ShapeDecoration( color: theme.colorScheme.secondary, - shape: context.isRtl + shape: Directionality.of(context) == TextDirection.rtl ? CustomRoundedRectangleBorder( leftSide: borderSide, rightSide: borderSide, @@ -188,53 +172,3 @@ class _MarkerArrowPainter extends CustomPainter { @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } - -class DotMarker extends StatelessWidget { - const DotMarker({Key? key}) : super(key: key); - - static const double diameter = 16; - static const double outerBorderRadiusDim = diameter; - static const outerBorderRadius = BorderRadius.all(Radius.circular(outerBorderRadiusDim)); - static const innerRadius = Radius.circular(outerBorderRadiusDim - ImageMarker.outerBorderWidth); - static const innerBorderRadius = BorderRadius.all(innerRadius); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; - final outerBorderColor = ImageMarker.themedOuterBorderColor(isDark); - final innerBorderColor = ImageMarker.themedInnerBorderColor(isDark); - - final outerDecoration = BoxDecoration( - border: Border.fromBorderSide(BorderSide( - color: outerBorderColor, - width: ImageMarker.outerBorderWidth, - )), - borderRadius: outerBorderRadius, - ); - - final innerDecoration = BoxDecoration( - border: Border.fromBorderSide(BorderSide( - color: innerBorderColor, - width: ImageMarker.innerBorderWidth, - )), - borderRadius: innerBorderRadius, - ); - - return Container( - decoration: outerDecoration, - child: DecoratedBox( - decoration: innerDecoration, - position: DecorationPosition.foreground, - child: ClipRRect( - borderRadius: innerBorderRadius, - child: Container( - width: diameter, - height: diameter, - color: theme.colorScheme.secondary, - ), - ), - ), - ); - } -} diff --git a/plugins/aves_map/lib/src/marker/key.dart b/plugins/aves_map/lib/src/marker/key.dart new file mode 100644 index 000000000..188ec010f --- /dev/null +++ b/plugins/aves_map/lib/src/marker/key.dart @@ -0,0 +1,13 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +@immutable +class MarkerKey extends LocalKey with EquatableMixin { + final T entry; + final int? count; + + @override + List get props => [entry, count]; + + const MarkerKey(this.entry, this.count); +} diff --git a/plugins/aves_map/lib/src/overlay/overlay.dart b/plugins/aves_map/lib/src/overlay/overlay.dart new file mode 100644 index 000000000..6fafa66f5 --- /dev/null +++ b/plugins/aves_map/lib/src/overlay/overlay.dart @@ -0,0 +1,17 @@ +import 'package:aves_map/src/overlay/tile.dart'; +import 'package:flutter/painting.dart'; +import 'package:latlong2/latlong.dart'; + +mixin MapOverlay { + String get id; + + bool get canOverlay; + + LatLng? get topLeft; + + LatLng? get bottomRight; + + ImageProvider get imageProvider; + + Future getTile(int tx, int ty, int? zoomLevel); +} diff --git a/lib/widgets/common/map/tile.dart b/plugins/aves_map/lib/src/overlay/tile.dart similarity index 100% rename from lib/widgets/common/map/tile.dart rename to plugins/aves_map/lib/src/overlay/tile.dart diff --git a/plugins/aves_map/lib/src/style.dart b/plugins/aves_map/lib/src/style.dart new file mode 100644 index 000000000..60cf96799 --- /dev/null +++ b/plugins/aves_map/lib/src/style.dart @@ -0,0 +1,14 @@ +enum EntryMapStyle { + // Google + googleNormal, + googleHybrid, + googleTerrain, + // Huawei + hmsNormal, + hmsTerrain, + // Leaflet + // browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/ + osmHot, + stamenToner, + stamenWatercolor, +} diff --git a/plugins/aves_map/lib/src/theme.dart b/plugins/aves_map/lib/src/theme.dart new file mode 100644 index 000000000..c0f75e67b --- /dev/null +++ b/plugins/aves_map/lib/src/theme.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +enum MapNavigationButton { back, map } + +class MapThemeData { + final bool interactive, showCoordinateFilter; + final MapNavigationButton navigationButton; + final Animation scale; + final VisualDensity? visualDensity; + final double? mapHeight; + + const MapThemeData({ + required this.interactive, + required this.showCoordinateFilter, + required this.navigationButton, + required this.scale, + required this.visualDensity, + required this.mapHeight, + }); + + static const double markerOuterBorderWidth = 1.5; + static const double markerInnerBorderWidth = 2; + static const double markerImageExtent = 48.0; + static const Size markerArrowSize = Size(8, 6); + + static Color markerThemedOuterBorderColor(bool isDark) => isDark ? Colors.white30 : Colors.black26; + + static Color markerThemedInnerBorderColor(bool isDark) => isDark ? const Color(0xFF212121) : Colors.white; +} diff --git a/lib/widgets/common/map/zoomed_bounds.dart b/plugins/aves_map/lib/src/zoomed_bounds.dart similarity index 98% rename from lib/widgets/common/map/zoomed_bounds.dart rename to plugins/aves_map/lib/src/zoomed_bounds.dart index b0d375145..0f835e3f4 100644 --- a/lib/widgets/common/map/zoomed_bounds.dart +++ b/plugins/aves_map/lib/src/zoomed_bounds.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:aves/utils/geo_utils.dart'; +import 'package:aves_map/src/geo_utils.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_map/flutter_map.dart'; diff --git a/plugins/aves_map/pubspec.yaml b/plugins/aves_map/pubspec.yaml new file mode 100644 index 000000000..b85fc4755 --- /dev/null +++ b/plugins/aves_map/pubspec.yaml @@ -0,0 +1,22 @@ +name: aves_map +version: 0.0.1 +publish_to: none + +environment: + sdk: ">=2.16.2 <3.0.0" + +dependencies: + flutter: + sdk: flutter + # TODO TLAD as of 2022/02/22, null safe version is pre-release + custom_rounded_rectangle_border: '>=0.2.0-nullsafety.0' + equatable: + fluster: + flutter_map: + latlong2: + provider: + +dev_dependencies: + flutter_lints: + +flutter: diff --git a/plugins/aves_report/pubspec.yaml b/plugins/aves_report/pubspec.yaml index 5ed692860..51d424e51 100644 --- a/plugins/aves_report/pubspec.yaml +++ b/plugins/aves_report/pubspec.yaml @@ -3,8 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" + sdk: ">=2.16.2 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_report_console/pubspec.yaml b/plugins/aves_report_console/pubspec.yaml index 00e9bccb9..927cb6806 100644 --- a/plugins/aves_report_console/pubspec.yaml +++ b/plugins/aves_report_console/pubspec.yaml @@ -3,8 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.17.0" + sdk: ">=2.16.2 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_report_crashlytics/pubspec.yaml b/plugins/aves_report_crashlytics/pubspec.yaml index 6e756d446..4f6a3e877 100644 --- a/plugins/aves_report_crashlytics/pubspec.yaml +++ b/plugins/aves_report_crashlytics/pubspec.yaml @@ -3,8 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.17.0" + sdk: ">=2.16.2 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_services/.gitignore b/plugins/aves_services/.gitignore new file mode 100644 index 000000000..9be145fde --- /dev/null +++ b/plugins/aves_services/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/plugins/aves_services/.metadata b/plugins/aves_services/.metadata new file mode 100644 index 000000000..c24d00d29 --- /dev/null +++ b/plugins/aves_services/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 5464c5bac742001448fe4fc0597be939379f88ea + channel: stable + +project_type: package diff --git a/plugins/aves_services/analysis_options.yaml b/plugins/aves_services/analysis_options.yaml new file mode 100644 index 000000000..f04c6cf0f --- /dev/null +++ b/plugins/aves_services/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/plugins/aves_services/lib/aves_services.dart b/plugins/aves_services/lib/aves_services.dart new file mode 100644 index 000000000..197267d2e --- /dev/null +++ b/plugins/aves_services/lib/aves_services.dart @@ -0,0 +1,31 @@ +library aves_services; + +import 'package:aves_map/aves_map.dart'; +import 'package:flutter/widgets.dart'; +import 'package:latlong2/latlong.dart'; + +abstract class MobileServices { + Future isServiceAvailable(); + + EntryMapStyle get defaultMapStyle; + + List get mapStyles; + + Widget buildMap({ + required AvesMapController? controller, + required Listenable clusterListenable, + required ValueNotifier boundsNotifier, + required EntryMapStyle style, + required TransitionBuilder decoratorBuilder, + required ButtonPanelBuilder buttonPanelBuilder, + required MarkerClusterBuilder markerClusterBuilder, + required MarkerWidgetBuilder markerWidgetBuilder, + required MarkerImageReadyChecker markerImageReadyChecker, + required ValueNotifier? dotLocationNotifier, + required ValueNotifier? overlayOpacityNotifier, + required MapOverlay? overlayEntry, + required UserZoomChangeCallback? onUserZoomChange, + required MapTapCallback? onMapTap, + required MarkerTapCallback? onMarkerTap, + }); +} diff --git a/plugins/aves_services/pubspec.yaml b/plugins/aves_services/pubspec.yaml new file mode 100644 index 000000000..d86da586d --- /dev/null +++ b/plugins/aves_services/pubspec.yaml @@ -0,0 +1,18 @@ +name: aves_services +version: 0.0.1 +publish_to: none + +environment: + sdk: ">=2.16.2 <3.0.0" + +dependencies: + flutter: + sdk: flutter + aves_map: + path: ../aves_map + latlong2: + +dev_dependencies: + flutter_lints: + +flutter: diff --git a/plugins/aves_services_google/.gitignore b/plugins/aves_services_google/.gitignore new file mode 100644 index 000000000..9be145fde --- /dev/null +++ b/plugins/aves_services_google/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/plugins/aves_services_google/.metadata b/plugins/aves_services_google/.metadata new file mode 100644 index 000000000..c24d00d29 --- /dev/null +++ b/plugins/aves_services_google/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 5464c5bac742001448fe4fc0597be939379f88ea + channel: stable + +project_type: package diff --git a/plugins/aves_services_google/analysis_options.yaml b/plugins/aves_services_google/analysis_options.yaml new file mode 100644 index 000000000..f04c6cf0f --- /dev/null +++ b/plugins/aves_services_google/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/plugins/aves_services_google/lib/aves_services_platform.dart b/plugins/aves_services_google/lib/aves_services_platform.dart new file mode 100644 index 000000000..bfe6d1bf8 --- /dev/null +++ b/plugins/aves_services_google/lib/aves_services_platform.dart @@ -0,0 +1,67 @@ +library aves_services_platform; + +import 'package:aves_map/aves_map.dart'; +import 'package:aves_services/aves_services.dart'; +import 'package:aves_services_platform/src/map.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:google_api_availability/google_api_availability.dart'; +import 'package:latlong2/latlong.dart'; + +class PlatformMobileServices extends MobileServices { + bool? _isAvailable; + + @override + Future isServiceAvailable() async { + if (_isAvailable != null) return SynchronousFuture(_isAvailable!); + final result = await GoogleApiAvailability.instance.checkGooglePlayServicesAvailability(); + _isAvailable = result == GooglePlayServicesAvailability.success; + debugPrint('Device has Google Play Services=$_isAvailable'); + return _isAvailable!; + } + + @override + EntryMapStyle get defaultMapStyle => EntryMapStyle.googleNormal; + + @override + List get mapStyles => [EntryMapStyle.googleNormal, EntryMapStyle.googleHybrid, EntryMapStyle.googleTerrain]; + + @override + Widget buildMap({ + required AvesMapController? controller, + required Listenable clusterListenable, + required ValueNotifier boundsNotifier, + required EntryMapStyle style, + required TransitionBuilder decoratorBuilder, + required ButtonPanelBuilder buttonPanelBuilder, + required MarkerClusterBuilder markerClusterBuilder, + required MarkerWidgetBuilder markerWidgetBuilder, + required MarkerImageReadyChecker markerImageReadyChecker, + required ValueNotifier? dotLocationNotifier, + required ValueNotifier? overlayOpacityNotifier, + required MapOverlay? overlayEntry, + required UserZoomChangeCallback? onUserZoomChange, + required MapTapCallback? onMapTap, + required MarkerTapCallback? onMarkerTap, + }) { + return EntryGoogleMap( + controller: controller, + clusterListenable: clusterListenable, + boundsNotifier: boundsNotifier, + minZoom: 0, + maxZoom: 20, + style: style, + decoratorBuilder: decoratorBuilder, + buttonPanelBuilder: buttonPanelBuilder, + markerClusterBuilder: markerClusterBuilder, + markerWidgetBuilder: markerWidgetBuilder, + markerImageReadyChecker: markerImageReadyChecker, + dotLocationNotifier: dotLocationNotifier, + overlayOpacityNotifier: overlayOpacityNotifier, + overlayEntry: overlayEntry, + onUserZoomChange: onUserZoomChange, + onMapTap: onMapTap, + onMarkerTap: onMarkerTap, + ); + } +} diff --git a/lib/widgets/common/map/google/map.dart b/plugins/aves_services_google/lib/src/map.dart similarity index 73% rename from lib/widgets/common/map/google/map.dart rename to plugins/aves_services_google/lib/src/map.dart index a5f9c620f..e7a78d395 100644 --- a/lib/widgets/common/map/google/map.dart +++ b/plugins/aves_services_google/lib/src/map.dart @@ -1,40 +1,29 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:aves/model/entry_images.dart'; -import 'package:aves/model/geotiff.dart'; -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/utils/change_notifier.dart'; -import 'package:aves/widgets/common/map/buttons.dart'; -import 'package:aves/widgets/common/map/controller.dart'; -import 'package:aves/widgets/common/map/decorator.dart'; -import 'package:aves/widgets/common/map/geo_entry.dart'; -import 'package:aves/widgets/common/map/geo_map.dart'; -import 'package:aves/widgets/common/map/google/geotiff_tile_provider.dart'; -import 'package:aves/widgets/common/map/google/marker_generator.dart'; -import 'package:aves/widgets/common/map/marker.dart'; -import 'package:aves/widgets/common/map/theme.dart'; -import 'package:aves/widgets/common/map/zoomed_bounds.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:latlong2/latlong.dart' as ll; import 'package:provider/provider.dart'; -class EntryGoogleMap extends StatefulWidget { +class EntryGoogleMap extends StatefulWidget { final AvesMapController? controller; final Listenable clusterListenable; final ValueNotifier boundsNotifier; final double? minZoom, maxZoom; final EntryMapStyle style; - final MarkerClusterBuilder markerClusterBuilder; - final MarkerWidgetBuilder markerWidgetBuilder; + final TransitionBuilder decoratorBuilder; + final ButtonPanelBuilder buttonPanelBuilder; + final MarkerClusterBuilder markerClusterBuilder; + final MarkerWidgetBuilder markerWidgetBuilder; + final MarkerImageReadyChecker markerImageReadyChecker; final ValueNotifier? dotLocationNotifier; final ValueNotifier? overlayOpacityNotifier; - final MappedGeoTiff? overlayEntry; + final MapOverlay? overlayEntry; final UserZoomChangeCallback? onUserZoomChange; - final void Function(ll.LatLng location)? onMapTap; - final void Function(GeoEntry geoEntry)? onMarkerTap; - final MapOpener? openMapPage; + final MapTapCallback? onMapTap; + final MarkerTapCallback? onMarkerTap; const EntryGoogleMap({ Key? key, @@ -44,27 +33,29 @@ class EntryGoogleMap extends StatefulWidget { this.minZoom, this.maxZoom, required this.style, + required this.decoratorBuilder, + required this.buttonPanelBuilder, required this.markerClusterBuilder, required this.markerWidgetBuilder, + required this.markerImageReadyChecker, required this.dotLocationNotifier, this.overlayOpacityNotifier, this.overlayEntry, this.onUserZoomChange, this.onMapTap, this.onMarkerTap, - this.openMapPage, }) : super(key: key); @override - State createState() => _EntryGoogleMapState(); + State createState() => _EntryGoogleMapState(); } -class _EntryGoogleMapState extends State with WidgetsBindingObserver { - GoogleMapController? _googleMapController; +class _EntryGoogleMapState extends State> with WidgetsBindingObserver { + GoogleMapController? _serviceMapController; final List _subscriptions = []; - Map _geoEntryByMarkerKey = {}; - final Map _markerBitmaps = {}; - final AChangeNotifier _markerBitmapChangeNotifier = AChangeNotifier(); + Map, GeoEntry> _geoEntryByMarkerKey = {}; + final Map, Uint8List> _markerBitmaps = {}; + final StreamController> _markerBitmapReadyStreamController = StreamController.broadcast(); Uint8List? _dotMarkerBitmap; ValueNotifier get boundsNotifier => widget.boundsNotifier; @@ -81,7 +72,7 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse } @override - void didUpdateWidget(covariant EntryGoogleMap oldWidget) { + void didUpdateWidget(covariant EntryGoogleMap oldWidget) { super.didUpdateWidget(oldWidget); _unregisterWidget(oldWidget); _registerWidget(widget); @@ -90,20 +81,20 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse @override void dispose() { _unregisterWidget(widget); - _googleMapController?.dispose(); + _serviceMapController?.dispose(); WidgetsBinding.instance!.removeObserver(this); super.dispose(); } - void _registerWidget(EntryGoogleMap widget) { + void _registerWidget(EntryGoogleMap widget) { final avesMapController = widget.controller; if (avesMapController != null) { - _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toGoogleLatLng(event.latLng)))); + _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toServiceLatLng(event.latLng)))); } widget.clusterListenable.addListener(_updateMarkers); } - void _unregisterWidget(EntryGoogleMap widget) { + void _unregisterWidget(EntryGoogleMap widget) { widget.clusterListenable.removeListener(_updateMarkers); _subscriptions ..forEach((sub) => sub.cancel()) @@ -120,7 +111,7 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse case AppLifecycleState.resumed: // workaround for blank Google map when resuming app // cf https://github.com/flutter/flutter/issues/40284 - _googleMapController?.setMapStyle(null); + _serviceMapController?.setMapStyle(null); break; } } @@ -134,31 +125,24 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse isReadyToRender: (key) => true, onRendered: (key, bitmap) => _dotMarkerBitmap = bitmap, ), - MarkerGeneratorWidget( + MarkerGeneratorWidget>( markers: _geoEntryByMarkerKey.keys.map(widget.markerWidgetBuilder).toList(), - isReadyToRender: (key) => key.entry.isThumbnailReady(extent: GeoMap.markerImageExtent), + isReadyToRender: widget.markerImageReadyChecker, onRendered: (key, bitmap) { _markerBitmaps[key] = bitmap; - _markerBitmapChangeNotifier.notify(); + _markerBitmapReadyStreamController.add(key); }, ), - MapDecorator( - child: _buildMap(), - ), - MapButtonPanel( - boundsNotifier: boundsNotifier, - zoomBy: _zoomBy, - openMapPage: widget.openMapPage, - resetRotation: _resetRotation, - ), + widget.decoratorBuilder(context, _buildMap()), + widget.buttonPanelBuilder(_zoomBy, _resetRotation), ], ); } Widget _buildMap() { - return AnimatedBuilder( - animation: _markerBitmapChangeNotifier, - builder: (context, child) { + return StreamBuilder( + stream: _markerBitmapReadyStreamController.stream, + builder: (context, _) { final markers = {}; _geoEntryByMarkerKey.forEach((markerKey, geoEntry) { final bytes = _markerBitmaps[markerKey]; @@ -185,11 +169,11 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse return GoogleMap( initialCameraPosition: CameraPosition( bearing: -bounds.rotation, - target: _toGoogleLatLng(bounds.projectedCenter), + target: _toServiceLatLng(bounds.projectedCenter), zoom: bounds.zoom, ), onMapCreated: (controller) async { - _googleMapController = controller; + _serviceMapController = controller; final zoom = await controller.getZoomLevel(); await _updateVisibleRegion(zoom: zoom, rotation: bounds.rotation); if (mounted) { @@ -220,7 +204,7 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse anchor: const Offset(.5, .5), consumeTapEvents: true, icon: BitmapDescriptor.fromBytes(_dotMarkerBitmap!), - position: _toGoogleLatLng(dotLocation), + position: _toServiceLatLng(dotLocation), zIndex: 1, ) }, @@ -228,14 +212,14 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse tileOverlays: { if (overlayEntry != null && overlayEntry.canOverlay) TileOverlay( - tileOverlayId: TileOverlayId(overlayEntry.entry.uri), - tileProvider: GeoTiffTileProvider(overlayEntry), + tileOverlayId: TileOverlayId(overlayEntry.id), + tileProvider: GmsGeoTiffTileProvider(overlayEntry), transparency: 1 - overlayOpacity, ), }, onCameraMove: (position) => _updateVisibleRegion(zoom: position.zoom, rotation: -position.bearing), onCameraIdle: _onIdle, - onTap: (position) => widget.onMapTap?.call(_fromGoogleLatLng(position)), + onTap: (position) => widget.onMapTap?.call(_fromServiceLatLng(position)), ); }, ); @@ -258,13 +242,13 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse Future _updateVisibleRegion({required double zoom, required double rotation}) async { if (!mounted) return; - final bounds = await _googleMapController?.getVisibleRegion(); + final bounds = await _serviceMapController?.getVisibleRegion(); if (bounds != null && (bounds.northeast != uninitializedLatLng || bounds.southwest != uninitializedLatLng)) { final sw = bounds.southwest; final ne = bounds.northeast; boundsNotifier.value = ZoomedBounds( - sw: _fromGoogleLatLng(sw), - ne: _fromGoogleLatLng(ne), + sw: _fromServiceLatLng(sw), + ne: _fromServiceLatLng(ne), zoom: zoom, rotation: rotation, ); @@ -278,17 +262,17 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse } Future _resetRotation() async { - final controller = _googleMapController; + final controller = _serviceMapController; if (controller == null) return; await controller.animateCamera(CameraUpdate.newCameraPosition(CameraPosition( - target: _toGoogleLatLng(bounds.projectedCenter), + target: _toServiceLatLng(bounds.projectedCenter), zoom: bounds.zoom, ))); } Future _zoomBy(double amount) async { - final controller = _googleMapController; + final controller = _serviceMapController; if (controller == null) return; widget.onUserZoomChange?.call(await controller.getZoomLevel() + amount); @@ -296,16 +280,16 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse } Future _moveTo(LatLng point) async { - final controller = _googleMapController; + final controller = _serviceMapController; if (controller == null) return; await controller.animateCamera(CameraUpdate.newLatLng(point)); } // `LatLng` used by `google_maps_flutter` is not the one from `latlong2` package - LatLng _toGoogleLatLng(ll.LatLng location) => LatLng(location.latitude, location.longitude); + LatLng _toServiceLatLng(ll.LatLng location) => LatLng(location.latitude, location.longitude); - ll.LatLng _fromGoogleLatLng(LatLng location) => ll.LatLng(location.latitude, location.longitude); + ll.LatLng _fromServiceLatLng(LatLng location) => ll.LatLng(location.latitude, location.longitude); MapType _toMapType(EntryMapStyle style) { switch (style) { @@ -320,3 +304,18 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse } } } + +class GmsGeoTiffTileProvider extends TileProvider { + MapOverlay overlayEntry; + + GmsGeoTiffTileProvider(this.overlayEntry); + + @override + Future getTile(int x, int y, int? zoom) async { + final tile = await overlayEntry.getTile(x, y, zoom); + if (tile != null) { + return Tile(tile.width, tile.height, tile.data); + } + return TileProvider.noTile; + } +} diff --git a/plugins/aves_services_google/pubspec.yaml b/plugins/aves_services_google/pubspec.yaml new file mode 100644 index 000000000..346ba9fea --- /dev/null +++ b/plugins/aves_services_google/pubspec.yaml @@ -0,0 +1,23 @@ +name: aves_services_platform +version: 0.0.1 +publish_to: none + +environment: + sdk: ">=2.16.2 <3.0.0" + +dependencies: + flutter: + sdk: flutter + aves_map: + path: ../aves_map + aves_services: + path: ../aves_services + google_api_availability: + google_maps_flutter: + latlong2: + provider: + +dev_dependencies: + flutter_lints: + +flutter: diff --git a/plugins/aves_services_huawei/.gitignore b/plugins/aves_services_huawei/.gitignore new file mode 100644 index 000000000..9be145fde --- /dev/null +++ b/plugins/aves_services_huawei/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/plugins/aves_services_huawei/.metadata b/plugins/aves_services_huawei/.metadata new file mode 100644 index 000000000..c24d00d29 --- /dev/null +++ b/plugins/aves_services_huawei/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 5464c5bac742001448fe4fc0597be939379f88ea + channel: stable + +project_type: package diff --git a/plugins/aves_services_huawei/analysis_options.yaml b/plugins/aves_services_huawei/analysis_options.yaml new file mode 100644 index 000000000..f04c6cf0f --- /dev/null +++ b/plugins/aves_services_huawei/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/plugins/aves_services_huawei/lib/aves_services_platform.dart b/plugins/aves_services_huawei/lib/aves_services_platform.dart new file mode 100644 index 000000000..be5ab6a14 --- /dev/null +++ b/plugins/aves_services_huawei/lib/aves_services_platform.dart @@ -0,0 +1,70 @@ +library aves_services_platform; + +import 'package:aves_map/aves_map.dart'; +import 'package:aves_services/aves_services.dart'; +import 'package:aves_services_platform/src/map.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:huawei_hmsavailability/huawei_hmsavailability.dart'; +import 'package:latlong2/latlong.dart'; + +class PlatformMobileServices extends MobileServices { + // cf https://developer.huawei.com/consumer/en/doc/development/hmscore-common-References/huaweiapiavailability-0000001050121134#section9492524178 + static const int _hmsCoreAvailable = 0; + + bool? _isAvailable; + + @override + Future isServiceAvailable() async { + if (_isAvailable != null) return SynchronousFuture(_isAvailable!); + final result = await HmsApiAvailability().isHMSAvailable(); + _isAvailable = result == _hmsCoreAvailable; + debugPrint('Device has Huawei Mobile Services=$_isAvailable'); + return _isAvailable!; + } + + @override + EntryMapStyle get defaultMapStyle => EntryMapStyle.hmsNormal; + + @override + List get mapStyles => [EntryMapStyle.hmsNormal, EntryMapStyle.hmsTerrain]; + + @override + Widget buildMap({ + required AvesMapController? controller, + required Listenable clusterListenable, + required ValueNotifier boundsNotifier, + required EntryMapStyle style, + required TransitionBuilder decoratorBuilder, + required ButtonPanelBuilder buttonPanelBuilder, + required MarkerClusterBuilder markerClusterBuilder, + required MarkerWidgetBuilder markerWidgetBuilder, + required MarkerImageReadyChecker markerImageReadyChecker, + required ValueNotifier? dotLocationNotifier, + required ValueNotifier? overlayOpacityNotifier, + required MapOverlay? overlayEntry, + required UserZoomChangeCallback? onUserZoomChange, + required MapTapCallback? onMapTap, + required MarkerTapCallback? onMarkerTap, + }) { + return EntryHmsMap( + controller: controller, + clusterListenable: clusterListenable, + boundsNotifier: boundsNotifier, + minZoom: 3, + maxZoom: 20, + style: style, + decoratorBuilder: decoratorBuilder, + buttonPanelBuilder: buttonPanelBuilder, + markerClusterBuilder: markerClusterBuilder, + markerWidgetBuilder: markerWidgetBuilder, + markerImageReadyChecker: markerImageReadyChecker, + dotLocationNotifier: dotLocationNotifier, + overlayOpacityNotifier: overlayOpacityNotifier, + overlayEntry: overlayEntry, + onUserZoomChange: onUserZoomChange, + onMapTap: onMapTap, + onMarkerTap: onMarkerTap, + ); + } +} diff --git a/plugins/aves_services_huawei/lib/src/map.dart b/plugins/aves_services_huawei/lib/src/map.dart new file mode 100644 index 000000000..7635e5ce8 --- /dev/null +++ b/plugins/aves_services_huawei/lib/src/map.dart @@ -0,0 +1,325 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:aves_map/aves_map.dart'; +import 'package:flutter/material.dart'; +import 'package:huawei_map/map.dart'; +import 'package:latlong2/latlong.dart' as ll; +import 'package:provider/provider.dart'; + +class EntryHmsMap extends StatefulWidget { + final AvesMapController? controller; + final Listenable clusterListenable; + final ValueNotifier boundsNotifier; + final double? minZoom, maxZoom; + final EntryMapStyle style; + final TransitionBuilder decoratorBuilder; + final ButtonPanelBuilder buttonPanelBuilder; + final MarkerClusterBuilder markerClusterBuilder; + final MarkerWidgetBuilder markerWidgetBuilder; + final MarkerImageReadyChecker markerImageReadyChecker; + final ValueNotifier? dotLocationNotifier; + final ValueNotifier? overlayOpacityNotifier; + final MapOverlay? overlayEntry; + final UserZoomChangeCallback? onUserZoomChange; + final MapTapCallback? onMapTap; + final MarkerTapCallback? onMarkerTap; + + const EntryHmsMap({ + Key? key, + this.controller, + required this.clusterListenable, + required this.boundsNotifier, + this.minZoom, + this.maxZoom, + required this.style, + required this.decoratorBuilder, + required this.buttonPanelBuilder, + required this.markerClusterBuilder, + required this.markerWidgetBuilder, + required this.markerImageReadyChecker, + required this.dotLocationNotifier, + this.overlayOpacityNotifier, + this.overlayEntry, + this.onUserZoomChange, + this.onMapTap, + this.onMarkerTap, + }) : super(key: key); + + @override + State createState() => _EntryHmsMapState(); +} + +class _EntryHmsMapState extends State> { + HuaweiMapController? _serviceMapController; + final List _subscriptions = []; + Map, GeoEntry> _geoEntryByMarkerKey = {}; + final Map, Uint8List> _markerBitmaps = {}; + final StreamController> _markerBitmapReadyStreamController = StreamController.broadcast(); + Uint8List? _dotMarkerBitmap; + + ValueNotifier get boundsNotifier => widget.boundsNotifier; + + ZoomedBounds get bounds => boundsNotifier.value; + + static const uninitializedLatLng = LatLng(0, 0); + + @override + void initState() { + super.initState(); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant EntryHmsMap oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(EntryHmsMap widget) { + final avesMapController = widget.controller; + if (avesMapController != null) { + _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toServiceLatLng(event.latLng)))); + } + widget.clusterListenable.addListener(_updateMarkers); + } + + void _unregisterWidget(EntryHmsMap widget) { + widget.clusterListenable.removeListener(_updateMarkers); + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + MarkerGeneratorWidget( + markers: const [DotMarker(key: Key('dot'))], + isReadyToRender: (key) => true, + onRendered: (key, bitmap) => _dotMarkerBitmap = bitmap, + ), + MarkerGeneratorWidget>( + markers: _geoEntryByMarkerKey.keys.map(widget.markerWidgetBuilder).toList(), + isReadyToRender: widget.markerImageReadyChecker, + onRendered: (key, bitmap) { + _markerBitmaps[key] = bitmap; + _markerBitmapReadyStreamController.add(key); + }, + ), + widget.decoratorBuilder(context, _buildMap()), + widget.buttonPanelBuilder(_zoomBy, _resetRotation), + ], + ); + } + + Widget _buildMap() { + return StreamBuilder( + stream: _markerBitmapReadyStreamController.stream, + builder: (context, _) { + final markers = {}; + _geoEntryByMarkerKey.forEach((markerKey, geoEntry) { + final bytes = _markerBitmaps[markerKey]; + if (bytes != null) { + final point = LatLng(geoEntry.latitude!, geoEntry.longitude!); + markers.add(Marker( + markerId: MarkerId(geoEntry.markerId!), + clickable: true, + icon: BitmapDescriptor.fromBytes(bytes), + position: point, + onClick: () => widget.onMarkerTap?.call(geoEntry), + )); + } + }); + + final interactive = context.select((v) => v.interactive); + // final overlayEntry = widget.overlayEntry; + return ValueListenableBuilder( + valueListenable: widget.dotLocationNotifier ?? ValueNotifier(null), + builder: (context, dotLocation, child) { + return ValueListenableBuilder( + valueListenable: widget.overlayOpacityNotifier ?? ValueNotifier(1), + builder: (context, overlayOpacity, child) { + return HuaweiMap( + initialCameraPosition: CameraPosition( + bearing: bounds.rotation, + target: _toServiceLatLng(bounds.projectedCenter), + zoom: bounds.zoom, + ), + mapType: _toMapType(widget.style), + // compass disabled to use provider agnostic controls + compassEnabled: false, + mapToolbarEnabled: false, + minMaxZoomPreference: MinMaxZoomPreference( + widget.minZoom ?? MinMaxZoomPreference.unbounded.minZoom, + widget.maxZoom ?? MinMaxZoomPreference.unbounded.maxZoom, + ), + // `allGesturesEnabled`, if defined overrides specific gesture settings + rotateGesturesEnabled: interactive, + scrollGesturesEnabled: interactive, + // zoom controls disabled to use provider agnostic controls + zoomControlsEnabled: false, + zoomGesturesEnabled: interactive, + // tilt disabled to match leaflet + tiltGesturesEnabled: false, + myLocationEnabled: false, + myLocationButtonEnabled: false, + trafficEnabled: false, + isScrollGesturesEnabledDuringRotateOrZoom: true, + markers: { + ...markers, + if (dotLocation != null && _dotMarkerBitmap != null) + Marker( + markerId: MarkerId('dot'), + anchor: const Offset(.5, .5), + clickable: true, + icon: BitmapDescriptor.fromBytes(_dotMarkerBitmap!), + position: _toServiceLatLng(dotLocation), + zIndex: 1, + ) + }, + // TODO TLAD [hms] GeoTIFF ground overlay + // groundOverlays: { + // if (overlayEntry != null && overlayEntry.canOverlay) + // GroundOverlay( + // groundOverlayId: GroundOverlayId('overlay'), + // // Google Maps API allows defining overlay either via + // // 1) position, anchor and width/height (in meters) + // // 2) bounds + // // Huawei requires width/height (in meters?), but also allows bounds... + // width: 42, + // height: 42, + // imageDescriptor: BitmapDescriptor.defaultMarker, + // position: _toServiceLatLng(overlayEntry.center!), + // ), + // }, + // TODO TLAD [hms] dynamic tile provider from current bounds, + // tileOverlays: { + // if (overlayEntry != null && overlayEntry.canOverlay) + // TileOverlay( + // tileOverlayId: TileOverlayId(overlayEntry.entry.uri), + // // `tileProvider` is `RepetitiveTile`, `UrlTile` or List + // // tileProvider: [ + // // Tile( + // // x: x, + // // y: y, + // // zoom: zoom, + // // imageData: imageData, + // // ), + // // ], + // transparency: 1 - overlayOpacity, + // ), + // }, + onMapCreated: (controller) async { + _serviceMapController = controller; + final zoom = await controller.getZoomLevel(); + await _updateVisibleRegion(zoom: zoom ?? bounds.zoom, rotation: bounds.rotation); + if (mounted) { + setState(() {}); + } + }, + onCameraMove: (position) => _updateVisibleRegion(zoom: position.zoom, rotation: position.bearing), + onCameraIdle: _onIdle, + onClick: (position) => widget.onMapTap?.call(_fromServiceLatLng(position)), + onPoiClick: (poi) { + final poiPosition = poi.latLng; + if (poiPosition != null) { + widget.onMapTap?.call(_fromServiceLatLng(poiPosition)); + } + }, + logoPadding: const EdgeInsets.all(8), + // lite mode disabled because it is not interactive + liteMode: false, + ); + }, + ); + }, + ); + }, + ); + } + + void _onIdle() { + if (!mounted) return; + widget.controller?.notifyIdle(bounds); + _updateMarkers(); + } + + void _updateMarkers() { + setState(() => _geoEntryByMarkerKey = widget.markerClusterBuilder()); + } + + Future _updateVisibleRegion({required double zoom, required double rotation}) async { + if (!mounted) return; + + final bounds = await _serviceMapController?.getVisibleRegion(); + if (bounds != null && (bounds.northeast != uninitializedLatLng || bounds.southwest != uninitializedLatLng)) { + final sw = bounds.southwest; + final ne = bounds.northeast; + boundsNotifier.value = ZoomedBounds( + sw: _fromServiceLatLng(sw), + ne: _fromServiceLatLng(ne), + zoom: zoom, + rotation: rotation, + ); + } else { + // the visible region is sometimes uninitialized when queried right after creation, + // so we query it again next frame + WidgetsBinding.instance!.addPostFrameCallback((_) { + _updateVisibleRegion(zoom: zoom, rotation: rotation); + }); + } + } + + Future _resetRotation() async { + final controller = _serviceMapController; + if (controller == null) return; + + await controller.animateCamera(CameraUpdate.newCameraPosition(CameraPosition( + target: _toServiceLatLng(bounds.projectedCenter), + zoom: bounds.zoom, + ))); + } + + Future _zoomBy(double amount) async { + final controller = _serviceMapController; + if (controller == null) return; + + final zoom = await controller.getZoomLevel(); + if (zoom == null) return; + + widget.onUserZoomChange?.call(zoom + amount); + await controller.animateCamera(CameraUpdate.zoomBy(amount)); + } + + Future _moveTo(LatLng point) async { + final controller = _serviceMapController; + if (controller == null) return; + + await controller.animateCamera(CameraUpdate.newLatLng(point)); + } + + // `LatLng` used by `google_maps_flutter` is not the one from `latlong2` package + LatLng _toServiceLatLng(ll.LatLng location) => LatLng(location.latitude, location.longitude); + + ll.LatLng _fromServiceLatLng(LatLng location) => ll.LatLng(location.lat, location.lng); + + MapType _toMapType(EntryMapStyle style) { + switch (style) { + case EntryMapStyle.hmsNormal: + return MapType.normal; + case EntryMapStyle.hmsTerrain: + return MapType.terrain; + default: + return MapType.none; + } + } +} diff --git a/plugins/aves_services_huawei/pubspec.yaml b/plugins/aves_services_huawei/pubspec.yaml new file mode 100644 index 000000000..d1831edd8 --- /dev/null +++ b/plugins/aves_services_huawei/pubspec.yaml @@ -0,0 +1,23 @@ +name: aves_services_platform +version: 0.0.1 +publish_to: none + +environment: + sdk: ">=2.16.2 <3.0.0" + +dependencies: + flutter: + sdk: flutter + aves_map: + path: ../aves_map + aves_services: + path: ../aves_services + huawei_hmsavailability: + huawei_map: + latlong2: + provider: + +dev_dependencies: + flutter_lints: + +flutter: diff --git a/pubspec.lock b/pubspec.lock index 69016d8d9..ddf65e899 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -36,6 +36,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.2" + aves_map: + dependency: "direct main" + description: + path: "plugins/aves_map" + relative: true + source: path + version: "0.0.1" aves_report: dependency: "direct main" description: @@ -50,6 +57,20 @@ packages: relative: true source: path version: "0.0.1" + aves_services: + dependency: "direct main" + description: + path: "plugins/aves_services" + relative: true + source: path + version: "0.0.1" + aves_services_platform: + dependency: "direct main" + description: + path: "plugins/aves_services_google" + relative: true + source: path + version: "0.0.1" barcode: dependency: transitive description: @@ -184,7 +205,7 @@ packages: source: hosted version: "3.0.1" custom_rounded_rectangle_border: - dependency: "direct main" + dependency: transitive description: name: custom_rounded_rectangle_border url: "https://pub.dartlang.org" @@ -305,7 +326,7 @@ packages: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "1.14.1" + version: "1.15.0" firebase_core_platform_interface: dependency: transitive description: @@ -326,14 +347,14 @@ packages: name: firebase_crashlytics url: "https://pub.dartlang.org" source: hosted - version: "2.6.2" + version: "2.6.3" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.2.3" + version: "3.2.4" flex_color_picker: dependency: "direct main" description: @@ -456,14 +477,14 @@ packages: source: hosted version: "2.0.2" google_api_availability: - dependency: "direct main" + dependency: transitive description: name: google_api_availability url: "https://pub.dartlang.org" source: hosted version: "3.0.1" google_maps_flutter: - dependency: "direct main" + dependency: transitive description: name: google_maps_flutter url: "https://pub.dartlang.org" @@ -1268,5 +1289,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.16.0 <3.0.0" + dart: ">=2.16.2 <3.0.0" flutter: ">=2.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index e09f2bcf1..2bb1f38f4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,7 +10,7 @@ version: 1.6.4+70 publish_to: none environment: - sdk: '>=2.16.0 <3.0.0' + sdk: ">=2.16.2 <3.0.0" # following https://github.blog/2021-09-01-improving-git-protocol-security-github/ # dependency GitHub repos should be referenced via `https://`, not `git://` @@ -21,16 +21,20 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter + aves_map: + path: plugins/aves_map aves_report: path: plugins/aves_report aves_report_platform: path: plugins/aves_report_crashlytics + aves_services: + path: plugins/aves_services + aves_services_platform: + path: plugins/aves_services_google charts_flutter: collection: connectivity_plus: country_code: -# TODO TLAD as of 2022/02/22, null safe version is pre-release - custom_rounded_rectangle_border: '>=0.2.0-nullsafety.0' decorated_icon: device_info_plus: equatable: @@ -50,8 +54,6 @@ dependencies: flutter_markdown: flutter_staggered_animations: get_it: - google_api_availability: - google_maps_flutter: intl: latlong2: material_design_icons_flutter: diff --git a/scripts/apply_flavor_huawei.sh b/scripts/apply_flavor_huawei.sh new file mode 100755 index 000000000..6a89cf577 --- /dev/null +++ b/scripts/apply_flavor_huawei.sh @@ -0,0 +1,9 @@ +#!/bin/bash +PUBSPEC_PATH="../pubspec.yaml" + +flutter clean + +sed -i 's/aves_services_google/aves_services_huawei/g' "$PUBSPEC_PATH" +sed -i 's/aves_report_crashlytics/aves_report_console/g' "$PUBSPEC_PATH" + +flutter pub get diff --git a/scripts/apply_flavor_izzy.sh b/scripts/apply_flavor_izzy.sh index 598f41175..31af6b867 100755 --- a/scripts/apply_flavor_izzy.sh +++ b/scripts/apply_flavor_izzy.sh @@ -3,6 +3,7 @@ PUBSPEC_PATH="../pubspec.yaml" flutter clean +sed -i 's/aves_services_huawei/aves_services_google/g' "$PUBSPEC_PATH" sed -i 's/aves_report_crashlytics/aves_report_console/g' "$PUBSPEC_PATH" flutter pub get diff --git a/scripts/apply_flavor_play.sh b/scripts/apply_flavor_play.sh index d613cc879..a02b9a00d 100755 --- a/scripts/apply_flavor_play.sh +++ b/scripts/apply_flavor_play.sh @@ -3,6 +3,7 @@ PUBSPEC_PATH="../pubspec.yaml" flutter clean +sed -i 's/aves_services_huawei/aves_services_google/g' "$PUBSPEC_PATH" sed -i 's/aves_report_console/aves_report_crashlytics/g' "$PUBSPEC_PATH" flutter pub get diff --git a/test/utils/geo_utils_test.dart b/test/utils/geo_utils_test.dart index 5f2cf7ecd..d1ef6c306 100644 --- a/test/utils/geo_utils_test.dart +++ b/test/utils/geo_utils_test.dart @@ -1,6 +1,6 @@ import 'package:aves/l10n/l10n.dart'; import 'package:aves/model/settings/enums/coordinate_format.dart'; -import 'package:aves/utils/geo_utils.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:test/test.dart'; diff --git a/test_driver/driver_screenshots.dart b/test_driver/driver_screenshots.dart index 515018fc6..e7817792f 100644 --- a/test_driver/driver_screenshots.dart +++ b/test_driver/driver_screenshots.dart @@ -3,6 +3,7 @@ import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/enums.dart'; +import 'package:aves_map/src/style.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/test_driver/driver_shaders.dart b/test_driver/driver_shaders.dart index ba95337da..8d973b7b3 100644 --- a/test_driver/driver_shaders.dart +++ b/test_driver/driver_shaders.dart @@ -4,6 +4,7 @@ import 'package:aves/main_play.dart' as app; import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves_map/src/style.dart'; import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_test/flutter_test.dart';