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