diff --git a/lib/locale/l10n/app_de.arb b/lib/locale/l10n/app_de.arb index 9af540e..3b22c36 100644 --- a/lib/locale/l10n/app_de.arb +++ b/lib/locale/l10n/app_de.arb @@ -62,6 +62,20 @@ "memorySheetUpdateMemoryMakePublic": "Veröffentlichen", "memorySheetUpdateMemoryMakePrivate": "Privat machen", "memorySheetDeleteMemory": "Erinnerung löschen", + "memorySheetViewMoreDetails": "Mehr Details", + + + "memoryMapScreenTitle": "Standort", + "memoryMapScreenExpandForMoreDescription": "Erweitere für mehr Details", + "memoryMapScreenOpenNavigation": "Navigation öffnen", + "memoryMapScreenValuesAddressLabel": "Geschätzte Adresse", + "memoryMapScreenValuesAddressIsLoading": "Adresse wird geladen...", + "memoryMapScreenValuesAddressIsUnavailable": "Address nicht verfügbar", + "memoryMapScreenValuesLatitudeLabel": "Latitude", + "memoryMapScreenValuesLongitudeLabel": "Longitude", + "memoryMapScreenValuesAltitudeLabel": "Höhe", + "memoryMapScreenValuesAccuracyLabel": "Genauigkeit", + "memoryMapScreenValuesSpeedLabel": "Geschwindigkeit", "emptyScreenTitle": "Houston, wir haben ein Problem", diff --git a/lib/locale/l10n/app_en.arb b/lib/locale/l10n/app_en.arb index 0937dbf..af54319 100644 --- a/lib/locale/l10n/app_en.arb +++ b/lib/locale/l10n/app_en.arb @@ -91,7 +91,44 @@ } }, "memorySheetMapEstimatedAddressLabel": "Estimated Address", - "memorySheetMapOpenNavigation": "Open Navigation", + "memorySheetViewMoreDetails": "View More Details", + + + "memoryMapScreenTitle": "Location", + "memoryMapScreenExpandForMoreDescription": "Expand for more details", + "memoryMapScreenOpenNavigation": "Open Navigation", + "memoryMapScreenValuesAddressLabel": "Estimated Address", + "memoryMapScreenValuesAddressIsLoading": "Loading address...", + "memoryMapScreenValuesAddressIsUnavailable": "Address not available", + "memoryMapScreenValuesLatitudeLabel": "Latitude", + "memoryMapScreenValuesLongitudeLabel": "Longitude", + "memoryMapScreenValuesAltitudeLabel": "Altitude", + "memoryMapScreenValuesAltitudeValue": "{valueInMeter}m", + "@memoryMapScreenValuesAltitudeValue": { + "placeholders": { + "valueInMeter": { + "type": "String" + } + } + }, + "memoryMapScreenValuesAccuracyLabel": "Accuracy", + "memoryMapScreenValuesAccuracyValue": "{valueInMeter}m", + "@memoryMapScreenValuesAccuracyValue": { + "placeholders": { + "valueInMeter": { + "type": "String" + } + } + }, + "memoryMapScreenValuesSpeedLabel": "Speed", + "memoryMapScreenValuesSpeedValue": "{valueInKmh} km/h", + "@memoryMapScreenValuesSpeedValue": { + "placeholders": { + "valueInKmh": { + "type": "String" + } + } + }, "emptyScreenTitle": "Houston, we have a problem", diff --git a/lib/screens/memory_map_screen.dart b/lib/screens/memory_map_screen.dart new file mode 100644 index 0000000..36547af --- /dev/null +++ b/lib/screens/memory_map_screen.dart @@ -0,0 +1,204 @@ +import 'package:expandable_bottom_sheet/expandable_bottom_sheet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_osm_plugin/flutter_osm_plugin.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:quid_faciam_hodie/constants/spacing.dart'; +import 'package:quid_faciam_hodie/foreign_types/memory_location.dart'; +import 'package:quid_faciam_hodie/utils/loadable.dart'; +import 'package:quid_faciam_hodie/utils/lookup_address.dart'; +import 'package:quid_faciam_hodie/utils/theme.dart'; +import 'package:quid_faciam_hodie/widgets/icon_button_child.dart'; +import 'package:quid_faciam_hodie/widgets/key_value_info.dart'; +import 'package:quid_faciam_hodie/widgets/sheet_indicator.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class MemoryMapScreen extends StatefulWidget { + final MemoryLocation location; + + const MemoryMapScreen({ + Key? key, + required this.location, + }) : super(key: key); + + @override + State createState() => _MemoryMapScreenState(); +} + +class _MemoryMapScreenState extends State with Loadable { + late final MapController controller; + String? address; + + @override + void initState() { + super.initState(); + + callWithLoading(fetchAddress); + + controller = MapController( + initPosition: GeoPoint( + latitude: widget.location.latitude, + longitude: widget.location.longitude, + ), + ); + } + + Future fetchAddress() async { + try { + final foundAddress = await lookupAddress( + latitude: widget.location.latitude, + longitude: widget.location.longitude, + ); + + setState(() { + address = foundAddress; + }); + } catch (error) { + setState(() { + address = null; + }); + } + } + + void drawCircle() => controller.drawCircle( + CircleOSM( + key: 'accuracy', + color: Colors.blue, + centerPoint: GeoPoint( + latitude: widget.location.latitude, + longitude: widget.location.longitude, + ), + radius: widget.location.accuracy, + strokeWidth: 4, + ), + ); + + List get staticPoints => [ + StaticPositionGeoPoint( + 'position', + const MarkerIcon( + icon: Icon(Icons.location_on, size: 150, color: Colors.blue), + ), + [ + GeoPoint( + latitude: widget.location.latitude, + longitude: widget.location.longitude, + ) + ], + ) + ]; + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + final backgroundColor = getSheetColor(context); + + return PlatformScaffold( + appBar: PlatformAppBar( + title: Text(localizations.memoryMapScreenTitle), + ), + body: ExpandableBottomSheet( + enableToggle: true, + background: OSMFlutter( + controller: controller, + initZoom: 13, + stepZoom: 1.0, + trackMyPosition: true, + staticPoints: staticPoints, + onMapIsReady: (_) { + drawCircle(); + }, + ), + persistentHeader: Container( + width: double.infinity, + padding: const EdgeInsets.all(SMALL_SPACE), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(LARGE_SPACE), + topRight: Radius.circular(LARGE_SPACE), + ), + ), + child: Padding( + padding: const EdgeInsets.all(MEDIUM_SPACE), + child: Column( + children: [ + const SheetIndicator(), + const SizedBox(height: MEDIUM_SPACE), + Text( + localizations.memoryMapScreenExpandForMoreDescription, + style: getBodyTextTextStyle(context), + ), + ], + ), + ), + ), + expandableContent: Container( + color: backgroundColor, + padding: const EdgeInsets.all(MEDIUM_SPACE), + child: Column( + children: [ + KeyValueInfo( + title: localizations.memoryMapScreenValuesAddressLabel, + value: () { + if (isLoading) { + return localizations.memoryMapScreenValuesAddressIsLoading; + } + + if (address == null) { + return localizations + .memoryMapScreenValuesAddressIsUnavailable; + } + + return address!; + }(), + ), + const SizedBox(height: SMALL_SPACE), + PlatformTextButton( + onPressed: () { + final url = + 'geo:0,0?q=${widget.location.latitude},${widget.location.longitude} (${address ?? ''})'; + launchUrl(Uri.parse(url)); + }, + child: IconButtonChild( + icon: Icon(context.platformIcons.location), + label: Text(localizations.memoryMapScreenOpenNavigation), + ), + ), + const SizedBox(height: SMALL_SPACE), + KeyValueInfo( + title: localizations.memoryMapScreenValuesLatitudeLabel, + value: widget.location.latitude.toString(), + ), + KeyValueInfo( + title: localizations.memoryMapScreenValuesLongitudeLabel, + value: widget.location.longitude.toString(), + ), + KeyValueInfo( + title: localizations.memoryMapScreenValuesAccuracyLabel, + value: localizations.memoryMapScreenValuesAccuracyValue( + widget.location.accuracy.toString(), + ), + icon: Icons.circle, + ), + KeyValueInfo( + title: localizations.memoryMapScreenValuesSpeedLabel, + value: localizations.memoryMapScreenValuesSpeedValue( + widget.location.speed.toString(), + ), + icon: Icons.speed_rounded, + ), + KeyValueInfo( + title: localizations.memoryMapScreenValuesAltitudeLabel, + value: localizations.memoryMapScreenValuesAltitudeValue( + widget.location.altitude.toString(), + ), + icon: Icons.landscape_rounded, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index c370bea..d01e5d0 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -128,12 +128,7 @@ class _SettingsScreenState extends AuthRequiredState Text( localizations .settingsScreenAccountSectionCreationDateLabel, - style: platformThemeData( - context, - material: (data) => data.textTheme.bodySmall, - cupertino: (data) => - data.textTheme.tabLabelTextStyle, - ), + style: getCaptionTextStyle(context), ) ], ), diff --git a/lib/screens/timeline_screen/memory_location_view.dart b/lib/screens/timeline_screen/memory_location_view.dart index cb7bab2..c8e4c70 100644 --- a/lib/screens/timeline_screen/memory_location_view.dart +++ b/lib/screens/timeline_screen/memory_location_view.dart @@ -1,17 +1,12 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_osm_plugin/flutter_osm_plugin.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:http/http.dart' as http; import 'package:quid_faciam_hodie/constants/spacing.dart'; import 'package:quid_faciam_hodie/constants/values.dart'; import 'package:quid_faciam_hodie/foreign_types/memory_location.dart'; -import 'package:quid_faciam_hodie/widgets/fade_and_move_in_animation.dart'; +import 'package:quid_faciam_hodie/screens/memory_map_screen.dart'; import 'package:quid_faciam_hodie/widgets/icon_button_child.dart'; -import 'package:quid_faciam_hodie/widgets/key_value_info.dart'; -import 'package:url_launcher/url_launcher.dart'; class MemoryLocationView extends StatefulWidget { final MemoryLocation location; @@ -27,14 +22,11 @@ class MemoryLocationView extends StatefulWidget { class _MemoryLocationViewState extends State { late final MapController controller; - String address = ''; @override void initState() { super.initState(); - lookupAddress(); - controller = MapController( initPosition: GeoPoint( latitude: widget.location.latitude, @@ -43,22 +35,11 @@ class _MemoryLocationViewState extends State { ); } - void lookupAddress() async { - final url = - 'https://geocode.maps.co/reverse?lat=${widget.location.latitude}&lon=${widget.location.longitude}'; - final uri = Uri.parse(url); + @override + void dispose() { + controller.dispose(); - final response = await http.get(uri); - - if (response.statusCode != 200) { - setState(() { - address = ''; - }); - } else { - setState(() { - address = jsonDecode(response.body)['display_name']; - }); - } + super.dispose(); } void drawCircle() => controller.drawCircle( @@ -104,45 +85,45 @@ class _MemoryLocationViewState extends State { SizedBox( width: double.infinity, height: 400, - child: OSMFlutter( - controller: controller, - initZoom: 14, - stepZoom: 1.0, - staticPoints: staticPoints, - onMapIsReady: (_) { - drawCircle(); - }, + child: GestureDetector( + // Avoid panning, map is view-only + onDoubleTap: () {}, + child: OSMFlutter( + controller: controller, + initZoom: 14, + minZoomLevel: 14, + maxZoomLevel: 14, + staticPoints: staticPoints, + onMapIsReady: (_) { + drawCircle(); + }, + ), ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: MEDIUM_SPACE), - child: Column( - children: [ - const SizedBox(height: MEDIUM_SPACE), - if (address.isNotEmpty) ...[ - FadeAndMoveInAnimation( - child: KeyValueInfo( - title: localizations.memorySheetMapEstimatedAddressLabel, - value: address, - icon: context.platformIcons.location, - ), - ), - const SizedBox(height: MEDIUM_SPACE), - ], - PlatformTextButton( - onPressed: () { - final url = - 'geo:0,0?q=${widget.location.latitude},${widget.location.longitude} ($address)'; - launchUrl(Uri.parse(url)); - }, - child: IconButtonChild( - icon: Icon(context.platformIcons.location), - label: Text(localizations.memorySheetMapOpenNavigation), + const SizedBox(height: MEDIUM_SPACE), + PlatformTextButton( + child: IconButtonChild( + icon: Icon(context.platformIcons.fullscreen), + label: Text(localizations.memorySheetViewMoreDetails), + ), + onPressed: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (_) => MemoryMapScreen( + location: widget.location, ), ), - ], - ), + ); + + if (!mounted) { + return; + } + + Navigator.pop(context); + }, ), + const SizedBox(height: MEDIUM_SPACE), ], ); } diff --git a/lib/screens/timeline_screen/memory_sheet.dart b/lib/screens/timeline_screen/memory_sheet.dart index 8f1af12..da57e1f 100644 --- a/lib/screens/timeline_screen/memory_sheet.dart +++ b/lib/screens/timeline_screen/memory_sheet.dart @@ -129,12 +129,7 @@ class _MemorySheetState extends State with Loadable { @override Widget build(BuildContext context) { final localizations = AppLocalizations.of(context)!; - final backgroundColor = platformThemeData( - context, - material: (data) => - data.bottomSheetTheme.modalBackgroundColor ?? data.bottomAppBarColor, - cupertino: (data) => data.barBackgroundColor, - ); + final backgroundColor = getSheetColor(context); return ExpandableBottomSheet( background: GestureDetector( @@ -151,13 +146,13 @@ class _MemorySheetState extends State with Loadable { ), child: Column( children: [ - const Padding( - padding: EdgeInsets.symmetric( - vertical: MEDIUM_SPACE, - horizontal: MEDIUM_SPACE, + if (widget.memory.location != null) ...[ + const Padding( + padding: EdgeInsets.all(SMALL_SPACE), + child: SheetIndicator(), ), - child: SheetIndicator(), - ), + const SizedBox(height: MEDIUM_SPACE), + ], Text( localizations.memorySheetTitle, style: getTitleTextStyle(context), @@ -224,43 +219,41 @@ class _MemorySheetState extends State with Loadable { ? buildLoadingIndicator() : null, ), - ], - ), - ), - expandableContent: Container( - width: double.infinity, - color: backgroundColor, - child: Column( - children: [ - widget.memory.location == null - ? const SizedBox.shrink() - : MemoryLocationView( - location: widget.memory.location!, + const SizedBox(height: MEDIUM_SPACE), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + context.platformIcons.time, + size: platformThemeData( + context, + material: (data) => data.textTheme.bodyLarge!.fontSize, + cupertino: (data) => data.textTheme.textStyle.fontSize, ), - Padding( - padding: const EdgeInsets.all(MEDIUM_SPACE), - child: Column( - children: [ - Text( - localizations.memorySheetCreatedAtDataKey( - DateFormat.jms().format( - widget.memory.creationDate, - ), + ), + const SizedBox(width: TINY_SPACE), + Text( + localizations.memorySheetCreatedAtDataKey( + DateFormat.jms().format( + widget.memory.creationDate, ), - style: getBodyTextTextStyle(context), ), - const SizedBox(height: SMALL_SPACE), - Text( - widget.memory.id, - textAlign: TextAlign.center, - style: getBodyTextTextStyle(context), - ), - ], - ), + style: getBodyTextTextStyle(context), + ), + ], ), ], ), ), + expandableContent: widget.memory.location == null + ? const SizedBox.shrink() + : Container( + width: double.infinity, + color: backgroundColor, + child: MemoryLocationView( + location: widget.memory.location!, + ), + ), ); } } diff --git a/lib/utils/lookup_address.dart b/lib/utils/lookup_address.dart new file mode 100644 index 0000000..524c6ce --- /dev/null +++ b/lib/utils/lookup_address.dart @@ -0,0 +1,15 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +Future lookupAddress({ + required final double latitude, + required final double longitude, +}) async { + final url = 'https://geocode.maps.co/reverse?lat=$latitude&lon=$longitude'; + final uri = Uri.parse(url); + + final response = await http.get(uri); + + return jsonDecode(response.body)['display_name']; +} diff --git a/lib/utils/theme.dart b/lib/utils/theme.dart index fa121a0..f496c8b 100644 --- a/lib/utils/theme.dart +++ b/lib/utils/theme.dart @@ -24,3 +24,16 @@ TextStyle getSubTitleTextStyle(final BuildContext context) => platformThemeData( material: (data) => data.textTheme.subtitle1!, cupertino: (data) => data.textTheme.navTitleTextStyle, ); + +TextStyle getCaptionTextStyle(final BuildContext context) => platformThemeData( + context, + material: (data) => data.textTheme.caption!, + cupertino: (data) => data.textTheme.tabLabelTextStyle, + ); + +Color getSheetColor(final BuildContext context) => platformThemeData( + context, + material: (data) => + data.bottomSheetTheme.modalBackgroundColor ?? data.bottomAppBarColor, + cupertino: (data) => data.barBackgroundColor, + );