refactor(mobile): maplibre (#6087)

* chore: maplibre gl pubspec

* refactor(wip): maplibre for maps

* refactor(wip): dual pane + location button

* chore: remove flutter_map and deps

* refactor(wip): map zoom to location

* refactor: location picker

* open gallery_viewer on marker tap

* remove detectScaleGesture param

* test: debounce and throttle

* chore: rename get location method

* feat(mobile): Adds gps locator to map prompt for easy geolocation (#6282)

* Refactored get gps coords

* Use var for linter's sake, should handle errors better

* Cleanup

* Fix linter issues

* chore(dep): update maplibre to official lib

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Joshua Herrera <joshua.herrera227@gmail.com>
This commit is contained in:
shenlong
2024-01-15 15:26:13 +00:00
committed by GitHub
parent aa8c54e248
commit e6c0f0e3aa
64 changed files with 2858 additions and 2171 deletions

View File

@@ -1,159 +1,138 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/map/models/map_state.model.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/utils/color_filter_generator.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:vector_map_tiles/vector_map_tiles.dart';
import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
class MapStateNotifier extends StateNotifier<MapState> {
MapStateNotifier(this._appSettingsProvider, this._apiService)
: super(
MapState(
isDarkTheme: _appSettingsProvider
.getSetting<bool>(AppSettingsEnum.mapThemeMode),
showFavoriteOnly: _appSettingsProvider
.getSetting<bool>(AppSettingsEnum.mapShowFavoriteOnly),
includeArchived: _appSettingsProvider
.getSetting<bool>(AppSettingsEnum.mapIncludeArchived),
relativeTime: _appSettingsProvider
.getSetting<int>(AppSettingsEnum.mapRelativeDate),
isLoading: true,
),
) {
_fetchStyleFromServer(
_appSettingsProvider.getSetting<bool>(AppSettingsEnum.mapThemeMode),
part 'map_state.provider.g.dart';
@Riverpod(keepAlive: true)
class MapStateNotifier extends _$MapStateNotifier {
final _log = Logger("MapStateNotifier");
@override
MapState build() {
final appSettingsProvider = ref.read(appSettingsServiceProvider);
// Fetch and save the Style JSONs
loadStyles();
return MapState(
themeMode: ThemeMode.values[
appSettingsProvider.getSetting<int>(AppSettingsEnum.mapThemeMode)],
showFavoriteOnly: appSettingsProvider
.getSetting<bool>(AppSettingsEnum.mapShowFavoriteOnly),
includeArchived: appSettingsProvider
.getSetting<bool>(AppSettingsEnum.mapIncludeArchived),
relativeTime:
appSettingsProvider.getSetting<int>(AppSettingsEnum.mapRelativeDate),
);
}
final AppSettingsService _appSettingsProvider;
final ApiService _apiService;
final Logger _log = Logger("MapStateNotifier");
void loadStyles() async {
final documents = (await getApplicationDocumentsDirectory()).path;
bool get isRaster =>
state.mapStyle != null && state.mapStyle!.rasterTileProvider != null;
// Set to loading
state = state.copyWith(lightStyleFetched: const AsyncLoading());
double get maxZoom =>
(isRaster ? state.mapStyle!.rasterTileProvider!.maximumZoom : 18)
.toDouble();
// Fetch and save light theme
final lightResponse = await ref
.read(apiServiceProvider)
.systemConfigApi
.getMapStyleWithHttpInfo(MapTheme.light);
void switchTheme(bool isDarkTheme) {
_updateThemeMode(isDarkTheme);
_fetchStyleFromServer(isDarkTheme);
}
void _updateThemeMode(bool isDarkTheme) {
_appSettingsProvider.setSetting(
AppSettingsEnum.mapThemeMode,
isDarkTheme,
);
state = state.copyWith(isDarkTheme: isDarkTheme, isLoading: true);
}
void _fetchStyleFromServer(bool isDarkTheme) async {
final styleResponse = await _apiService.systemConfigApi
.getMapStyleWithHttpInfo(isDarkTheme ? MapTheme.dark : MapTheme.light);
if (styleResponse.statusCode >= HttpStatus.badRequest) {
throw ApiException(styleResponse.statusCode, styleResponse.body);
}
final styleJsonString = styleResponse.body.isNotEmpty &&
styleResponse.statusCode != HttpStatus.noContent
? styleResponse.body
: null;
if (styleJsonString == null) {
_log.severe('Style JSON from server is empty');
if (lightResponse.statusCode >= HttpStatus.badRequest) {
state = state.copyWith(
lightStyleFetched: AsyncError(lightResponse.body, StackTrace.current),
);
_log.severe(
"Cannot fetch map light style with status - ${lightResponse.statusCode} and response - ${lightResponse.body}",
);
return;
}
final styleJson = await compute(jsonDecode, styleJsonString);
if (styleJson is! Map<String, dynamic>) {
_log.severe('Style JSON from server is invalid');
final lightJSON = lightResponse.body;
final lightFile = await File("$documents/map-style-light.json")
.writeAsString(lightJSON, flush: true);
// Update state with path
state =
state.copyWith(lightStyleFetched: AsyncData(lightFile.absolute.path));
// Set to loading
state = state.copyWith(darkStyleFetched: const AsyncLoading());
// Fetch and save dark theme
final darkResponse = await ref
.read(apiServiceProvider)
.systemConfigApi
.getMapStyleWithHttpInfo(MapTheme.dark);
if (darkResponse.statusCode >= HttpStatus.badRequest) {
state = state.copyWith(
darkStyleFetched: AsyncError(darkResponse.body, StackTrace.current),
);
_log.severe(
"Cannot fetch map dark style with status - ${darkResponse.statusCode} and response - ${darkResponse.body}",
);
return;
}
final styleReader = StyleReader(uri: '');
Style? style;
try {
style = await styleReader.readFromMap(styleJson);
} finally {
// Consume all error
}
state = state.copyWith(
mapStyle: style,
isLoading: false,
);
final darkJSON = darkResponse.body;
final darkFile = await File("$documents/map-style-dark.json")
.writeAsString(darkJSON, flush: true);
// Update state with path
state = state.copyWith(darkStyleFetched: AsyncData(darkFile.absolute.path));
}
void switchTheme(ThemeMode mode) {
ref.read(appSettingsServiceProvider).setSetting(
AppSettingsEnum.mapThemeMode,
mode.index,
);
state = state.copyWith(themeMode: mode);
}
void switchFavoriteOnly(bool isFavoriteOnly) {
_appSettingsProvider.setSetting(
AppSettingsEnum.mapShowFavoriteOnly,
isFavoriteOnly,
ref.read(appSettingsServiceProvider).setSetting(
AppSettingsEnum.mapShowFavoriteOnly,
isFavoriteOnly,
);
state = state.copyWith(
showFavoriteOnly: isFavoriteOnly,
shouldRefetchMarkers: true,
);
state = state.copyWith(showFavoriteOnly: isFavoriteOnly);
}
void setRefetchMarkers(bool shouldRefetch) {
state = state.copyWith(shouldRefetchMarkers: shouldRefetch);
}
void switchIncludeArchived(bool isIncludeArchived) {
_appSettingsProvider.setSetting(
AppSettingsEnum.mapIncludeArchived,
isIncludeArchived,
ref.read(appSettingsServiceProvider).setSetting(
AppSettingsEnum.mapIncludeArchived,
isIncludeArchived,
);
state = state.copyWith(
includeArchived: isIncludeArchived,
shouldRefetchMarkers: true,
);
state = state.copyWith(includeArchived: isIncludeArchived);
}
void setRelativeTime(int relativeTime) {
_appSettingsProvider.setSetting(
AppSettingsEnum.mapRelativeDate,
relativeTime,
ref.read(appSettingsServiceProvider).setSetting(
AppSettingsEnum.mapRelativeDate,
relativeTime,
);
state = state.copyWith(
relativeTime: relativeTime,
shouldRefetchMarkers: true,
);
state = state.copyWith(relativeTime: relativeTime);
}
Widget getTileLayer([bool forceDark = false]) {
if (isRaster) {
final rasterProvider = state.mapStyle!.rasterTileProvider;
final rasterLayer = TileLayer(
urlTemplate: rasterProvider!.url,
maxNativeZoom: rasterProvider.maximumZoom,
maxZoom: rasterProvider.maximumZoom.toDouble(),
);
return state.isDarkTheme || forceDark
? InvertionFilter(
child: SaturationFilter(
saturation: -1,
child: BrightnessFilter(
brightness: -1,
child: rasterLayer,
),
),
)
: rasterLayer;
}
if (state.mapStyle != null && !isRaster) {
return VectorTileLayer(
// Tiles and themes will be set for vector providers
tileProviders: state.mapStyle!.providers!,
theme: state.mapStyle!.theme!,
sprites: state.mapStyle!.sprites,
concurrency: 6,
);
}
return const Center(child: ImmichLoadingIndicator());
}
}
final mapStateNotifier =
StateNotifierProvider<MapStateNotifier, MapState>((ref) {
return MapStateNotifier(
ref.watch(appSettingsServiceProvider),
ref.watch(apiServiceProvider),
);
});