mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
feat(mobile): edit date time & location (#5461)
* chore: text correction * fix: update activities stat only when the widget is mounted * feat(mobile): edit date time * feat(mobile): edit location * chore(build): update gradle wrapper - 7.6.3 * style: dropdownmenu styling * style: wrap locationpicker in singlechildscrollview * test: add unit test for getTZAdjustedTimeAndOffset * pr changes --------- Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
parent
84c5b08c25
commit
086a957a2b
22 changed files with 1211 additions and 124 deletions
113
mobile/lib/modules/map/ui/map_location_picker.dart
Normal file
113
mobile/lib/modules/map/ui/map_location_picker.dart
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
import 'package:immich_mobile/utils/immich_app_theme.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
class MapLocationPickerPage extends HookConsumerWidget {
|
||||
final LatLng? initialLatLng;
|
||||
|
||||
const MapLocationPickerPage({super.key, this.initialLatLng});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedLatLng = useState<LatLng>(initialLatLng ?? LatLng(0, 0));
|
||||
final isDarkTheme =
|
||||
ref.watch(mapStateNotifier.select((state) => state.isDarkTheme));
|
||||
final isLoading =
|
||||
ref.watch(mapStateNotifier.select((state) => state.isLoading));
|
||||
final maxZoom = ref.read(mapStateNotifier.notifier).maxZoom;
|
||||
|
||||
return Theme(
|
||||
// Override app theme based on map theme
|
||||
data: isDarkTheme ? immichDarkTheme : immichLightTheme,
|
||||
child: Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
body: Stack(
|
||||
children: [
|
||||
if (!isLoading)
|
||||
FlutterMap(
|
||||
options: MapOptions(
|
||||
maxBounds:
|
||||
LatLngBounds(LatLng(-90, -180.0), LatLng(90.0, 180.0)),
|
||||
interactiveFlags: InteractiveFlag.doubleTapZoom |
|
||||
InteractiveFlag.drag |
|
||||
InteractiveFlag.flingAnimation |
|
||||
InteractiveFlag.pinchMove |
|
||||
InteractiveFlag.pinchZoom,
|
||||
center: LatLng(20, 20),
|
||||
zoom: 2,
|
||||
minZoom: 1,
|
||||
maxZoom: maxZoom,
|
||||
onTap: (tapPosition, point) => selectedLatLng.value = point,
|
||||
),
|
||||
children: [
|
||||
ref.read(mapStateNotifier.notifier).getTileLayer(),
|
||||
MarkerLayer(
|
||||
markers: [
|
||||
Marker(
|
||||
anchorPos: AnchorPos.align(AnchorAlign.top),
|
||||
point: selectedLatLng.value,
|
||||
builder: (ctx) => const Image(
|
||||
image: AssetImage('assets/location-pin.png'),
|
||||
),
|
||||
height: 40,
|
||||
width: 40,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isLoading)
|
||||
Positioned(
|
||||
top: context.height * 0.35,
|
||||
left: context.width * 0.425,
|
||||
child: const ImmichLoadingIndicator(),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomSheet: BottomSheet(
|
||||
onClosing: () {},
|
||||
builder: (context) => SizedBox(
|
||||
height: 150,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"${selectedLatLng.value.latitude.toStringAsFixed(4)}, ${selectedLatLng.value.longitude.toStringAsFixed(4)}",
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.primaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () => context.autoPop(selectedLatLng.value),
|
||||
child: const Text("map_location_picker_page_use_location")
|
||||
.tr(),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.autoPop(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: context.colorScheme.error,
|
||||
),
|
||||
child: const Text("action_common_cancel").tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_map/plugin_api.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
|
||||
import 'package:immich_mobile/modules/map/utils/map_controller_hook.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
|
|
@ -12,13 +14,15 @@ class MapThumbnail extends HookConsumerWidget {
|
|||
final double zoom;
|
||||
final List<Marker> markers;
|
||||
final double height;
|
||||
final double width;
|
||||
final bool showAttribution;
|
||||
final bool isDarkTheme;
|
||||
|
||||
const MapThumbnail({
|
||||
super.key,
|
||||
required this.coords,
|
||||
required this.height,
|
||||
this.height = 100,
|
||||
this.width = 100,
|
||||
this.onTap,
|
||||
this.zoom = 1,
|
||||
this.showAttribution = true,
|
||||
|
|
@ -28,18 +32,33 @@ class MapThumbnail extends HookConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final mapController = useMapController();
|
||||
final isMapReady = useRef(false);
|
||||
ref.watch(mapStateNotifier.select((s) => s.mapStyle));
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
if (isMapReady.value && mapController.center != coords) {
|
||||
mapController.move(coords, zoom);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[coords],
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
height: height,
|
||||
width: width,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||
child: FlutterMap(
|
||||
mapController: mapController,
|
||||
options: MapOptions(
|
||||
interactiveFlags: InteractiveFlag.none,
|
||||
center: coords,
|
||||
zoom: zoom,
|
||||
onTap: onTap,
|
||||
onMapReady: () => isMapReady.value = true,
|
||||
),
|
||||
nonRotatedChildren: [
|
||||
if (showAttribution)
|
||||
|
|
|
|||
32
mobile/lib/modules/map/utils/map_controller_hook.dart
Normal file
32
mobile/lib/modules/map/utils/map_controller_hook.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
|
||||
MapController useMapController({
|
||||
String? debugLabel,
|
||||
List<Object?>? keys,
|
||||
}) {
|
||||
return use(_MapControllerHook(keys: keys));
|
||||
}
|
||||
|
||||
class _MapControllerHook extends Hook<MapController> {
|
||||
const _MapControllerHook({List<Object?>? keys}) : super(keys: keys);
|
||||
|
||||
@override
|
||||
HookState<MapController, Hook<MapController>> createState() =>
|
||||
_MapControllerHookState();
|
||||
}
|
||||
|
||||
class _MapControllerHookState
|
||||
extends HookState<MapController, _MapControllerHook> {
|
||||
late final controller = MapController();
|
||||
|
||||
@override
|
||||
MapController build(BuildContext context) => controller;
|
||||
|
||||
@override
|
||||
void dispose() => controller.dispose();
|
||||
|
||||
@override
|
||||
String get debugLabel => 'useMapController';
|
||||
}
|
||||
|
|
@ -55,6 +55,7 @@ class MapPageState extends ConsumerState<MapPage> {
|
|||
// in onMapEvent() since MapEventMove#id is not populated properly in the
|
||||
// current version of flutter_map(4.0.0) used
|
||||
bool forceAssetUpdate = false;
|
||||
bool isMapReady = false;
|
||||
late final Debounce debounce;
|
||||
|
||||
@override
|
||||
|
|
@ -79,7 +80,7 @@ class MapPageState extends ConsumerState<MapPage> {
|
|||
bool forceReload = false,
|
||||
}) {
|
||||
try {
|
||||
final bounds = mapController.bounds;
|
||||
final bounds = isMapReady ? mapController.bounds : null;
|
||||
if (bounds != null) {
|
||||
final oldAssetsInBounds = assetsInBounds.toSet();
|
||||
assetsInBounds =
|
||||
|
|
@ -455,6 +456,7 @@ class MapPageState extends ConsumerState<MapPage> {
|
|||
minZoom: 1,
|
||||
maxZoom: maxZoom,
|
||||
onMapReady: () {
|
||||
isMapReady = true;
|
||||
mapController.mapEventStream.listen(onMapEvent);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue