mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 11:24:37 +02:00
e0864768c2
* refactor(mobile): make location picker scaffold primary * chore(mobile): update map heatmap colors * style(mobile): map bottomsheet - only use borders on top * fix(mobile): location picker show buttons above navigation bar * fix: crash on iOS due to heatmap invalid color format * disable rotate --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
183 lines
5.8 KiB
Dart
183 lines
5.8 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:auto_route/auto_route.dart';
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
|
import 'package:immich_mobile/modules/map/widgets/map_theme_override.dart';
|
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
|
import 'package:immich_mobile/modules/map/utils/map_utils.dart';
|
|
|
|
@RoutePage<LatLng?>()
|
|
class MapLocationPickerPage extends HookConsumerWidget {
|
|
final LatLng initialLatLng;
|
|
|
|
const MapLocationPickerPage({
|
|
super.key,
|
|
this.initialLatLng = const LatLng(0, 0),
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final selectedLatLng = useValueNotifier<LatLng>(initialLatLng);
|
|
final controller = useRef<MaplibreMapController?>(null);
|
|
final marker = useRef<Symbol?>(null);
|
|
|
|
Future<void> onStyleLoaded() async {
|
|
marker.value = await controller.value?.addMarkerAtLatLng(initialLatLng);
|
|
}
|
|
|
|
Future<void> onMapClick(Point<num> point, LatLng centre) async {
|
|
selectedLatLng.value = centre;
|
|
controller.value?.animateCamera(CameraUpdate.newLatLng(centre));
|
|
if (marker.value != null) {
|
|
await controller.value
|
|
?.updateSymbol(marker.value!, SymbolOptions(geometry: centre));
|
|
}
|
|
}
|
|
|
|
void onClose([LatLng? selected]) {
|
|
context.popRoute(selected);
|
|
}
|
|
|
|
Future<void> getCurrentLocation() async {
|
|
var (currentLocation, _) =
|
|
await MapUtils.checkPermAndGetLocation(context);
|
|
|
|
if (currentLocation == null) {
|
|
return;
|
|
}
|
|
|
|
var currentLatLng =
|
|
LatLng(currentLocation.latitude, currentLocation.longitude);
|
|
selectedLatLng.value = currentLatLng;
|
|
controller.value?.animateCamera(CameraUpdate.newLatLng(currentLatLng));
|
|
}
|
|
|
|
return MapThemeOveride(
|
|
mapBuilder: (style) => Builder(
|
|
builder: (ctx) => Scaffold(
|
|
backgroundColor: ctx.themeData.cardColor,
|
|
appBar: _AppBar(onClose: onClose),
|
|
extendBodyBehindAppBar: true,
|
|
primary: true,
|
|
body: style.widgetWhen(
|
|
onData: (style) => Container(
|
|
clipBehavior: Clip.antiAliasWithSaveLayer,
|
|
decoration: const BoxDecoration(
|
|
borderRadius: BorderRadius.only(
|
|
bottomLeft: Radius.circular(40),
|
|
bottomRight: Radius.circular(40),
|
|
),
|
|
),
|
|
child: MaplibreMap(
|
|
initialCameraPosition:
|
|
CameraPosition(target: initialLatLng, zoom: 12),
|
|
styleString: style,
|
|
onMapCreated: (mapController) =>
|
|
controller.value = mapController,
|
|
onStyleLoadedCallback: onStyleLoaded,
|
|
onMapClick: onMapClick,
|
|
dragEnabled: false,
|
|
tiltGesturesEnabled: false,
|
|
myLocationEnabled: false,
|
|
attributionButtonMargins: const Point(20, 15),
|
|
),
|
|
),
|
|
),
|
|
bottomNavigationBar: _BottomBar(
|
|
selectedLatLng: selectedLatLng,
|
|
onUseLocation: () => onClose(selectedLatLng.value),
|
|
onGetCurrentLocation: getCurrentLocation,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
|
|
final Function() onClose;
|
|
|
|
const _AppBar({required this.onClose});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 25),
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: ElevatedButton(
|
|
onPressed: onClose,
|
|
style: ElevatedButton.styleFrom(
|
|
shape: const CircleBorder(),
|
|
),
|
|
child: const Icon(Icons.arrow_back_ios_new_rounded),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Size get preferredSize => const Size.fromHeight(100);
|
|
}
|
|
|
|
class _BottomBar extends StatelessWidget {
|
|
final ValueNotifier<LatLng> selectedLatLng;
|
|
final Function() onUseLocation;
|
|
final Function() onGetCurrentLocation;
|
|
|
|
const _BottomBar({
|
|
required this.selectedLatLng,
|
|
required this.onUseLocation,
|
|
required this.onGetCurrentLocation,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox(
|
|
height: 150 + context.padding.bottom,
|
|
child: Padding(
|
|
padding: EdgeInsets.only(bottom: context.padding.bottom),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.public, size: 18),
|
|
const SizedBox(width: 15),
|
|
ValueListenableBuilder(
|
|
valueListenable: selectedLatLng,
|
|
builder: (_, value, __) => Text(
|
|
"${value.latitude.toStringAsFixed(4)}, ${value.longitude.toStringAsFixed(4)}",
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
ElevatedButton(
|
|
onPressed: onUseLocation,
|
|
child:
|
|
const Text("map_location_picker_page_use_location").tr(),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: onGetCurrentLocation,
|
|
child: const Icon(Icons.my_location),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|