mirror of
https://github.com/immich-app/immich.git
synced 2025-12-22 01:11:20 +03:00
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:
@@ -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),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user