From f0573620a8940117102c6439fba75e382898e411 Mon Sep 17 00:00:00 2001 From: Kemal Idris Date: Fri, 7 Feb 2025 22:55:44 +0800 Subject: [PATCH] fix: custom maps dialog confirmation failure ds-33 (#34) * refactor(lib-interfaces): rename route paths constants ds-33 * feat(lib-interfaces): add mixed stories (with & without location fetch combined) ds-33 - Also cache post story picked image state on web reload. * fix: fix failed custom maps dialog confirmation on post story ds-33 --- lib/domain/entities/story_entity.dart | 11 +- lib/interfaces/libs/constants.dart | 8 +- .../dark_google_maps_style_constants.dart | 1 + .../app_route_paths_constant.dart | 0 .../auth_route_paths_constant.dart} | 2 +- .../profile_route_paths_constant.dart} | 2 +- .../stories_route_paths_constant.dart} | 2 +- .../modules/picked_image_provider.dart | 43 +- .../providers/modules/stories_provider.dart | 26 +- lib/interfaces/libs/widgets.dart | 2 +- .../libs/widgets/maps/custom_maps_dialog.dart | 417 ----------------- .../libs/widgets/maps/custom_maps_view.dart | 437 ++++++++++++++++++ .../widgets/maps/get_location_dialog.dart | 259 ++++++----- .../modules/routes/auth/sign_in_route.dart | 2 +- .../modules/routes/auth/sign_up_route.dart | 2 +- .../root/stories/routes/post_story_route.dart | 36 +- .../stories/routes/story_detail_route.dart | 10 +- .../routes/root/stories/stories_route.dart | 4 +- .../modules/stories/get_stories_use_case.dart | 2 +- .../stories/get_story_by_id_use_case.dart | 16 +- 20 files changed, 707 insertions(+), 575 deletions(-) rename lib/interfaces/libs/constants/{ => route_paths}/app_route_paths_constant.dart (100%) rename lib/interfaces/libs/constants/{app_route_auth_paths_constant.dart => route_paths/auth_route_paths_constant.dart} (78%) rename lib/interfaces/libs/constants/{app_route_profile_paths_constant.dart => route_paths/profile_route_paths_constant.dart} (71%) rename lib/interfaces/libs/constants/{app_route_stories_paths_constant.dart => route_paths/stories_route_paths_constant.dart} (81%) delete mode 100644 lib/interfaces/libs/widgets/maps/custom_maps_dialog.dart create mode 100644 lib/interfaces/libs/widgets/maps/custom_maps_view.dart diff --git a/lib/domain/entities/story_entity.dart b/lib/domain/entities/story_entity.dart index 903e36e..a9a0723 100644 --- a/lib/domain/entities/story_entity.dart +++ b/lib/domain/entities/story_entity.dart @@ -1,5 +1,5 @@ -import "package:flutter/foundation.dart"; import "package:freezed_annotation/freezed_annotation.dart"; +import "package:equatable/equatable.dart"; import "package:dicoding_story_fl/libs/decorators.dart"; import "location_data_entity.dart"; @@ -7,8 +7,8 @@ import "location_data_entity.dart"; part "story_entity.freezed.dart"; part "story_entity.g.dart"; -@Freezed(copyWith: true) -class Story with _$Story { +@Freezed(copyWith: true, equal: false) +class Story with _$Story, EquatableMixin { const factory Story({ required String id, @JsonKey(name: "name") required String owner, @@ -22,5 +22,10 @@ class Story with _$Story { @ignoreJsonSerializable LocationData? location, }) = _Story; + const Story._(); + factory Story.fromJson(Map json) => _$StoryFromJson(json); + + @override + List get props => [id]; } diff --git a/lib/interfaces/libs/constants.dart b/lib/interfaces/libs/constants.dart index 0e9d40e..e7412ed 100644 --- a/lib/interfaces/libs/constants.dart +++ b/lib/interfaces/libs/constants.dart @@ -1,6 +1,6 @@ -export "constants/app_route_auth_paths_constant.dart"; -export "constants/app_route_paths_constant.dart"; -export "constants/app_route_profile_paths_constant.dart"; -export "constants/app_route_stories_paths_constant.dart"; +export "constants/route_paths/auth_route_paths_constant.dart"; +export "constants/route_paths/app_route_paths_constant.dart"; +export "constants/route_paths/profile_route_paths_constant.dart"; +export "constants/route_paths/stories_route_paths_constant.dart"; export "constants/dark_google_maps_style_constants.dart"; export "constants/date_format_constant.dart"; diff --git a/lib/interfaces/libs/constants/dark_google_maps_style_constants.dart b/lib/interfaces/libs/constants/dark_google_maps_style_constants.dart index 5951005..8247229 100644 --- a/lib/interfaces/libs/constants/dark_google_maps_style_constants.dart +++ b/lib/interfaces/libs/constants/dark_google_maps_style_constants.dart @@ -1,3 +1,4 @@ +/// https://mapstyle.withgoogle.com/ const kDarkGoogleMapsStyle = ''' [ { diff --git a/lib/interfaces/libs/constants/app_route_paths_constant.dart b/lib/interfaces/libs/constants/route_paths/app_route_paths_constant.dart similarity index 100% rename from lib/interfaces/libs/constants/app_route_paths_constant.dart rename to lib/interfaces/libs/constants/route_paths/app_route_paths_constant.dart diff --git a/lib/interfaces/libs/constants/app_route_auth_paths_constant.dart b/lib/interfaces/libs/constants/route_paths/auth_route_paths_constant.dart similarity index 78% rename from lib/interfaces/libs/constants/app_route_auth_paths_constant.dart rename to lib/interfaces/libs/constants/route_paths/auth_route_paths_constant.dart index 5bb039b..8bcc1e7 100644 --- a/lib/interfaces/libs/constants/app_route_auth_paths_constant.dart +++ b/lib/interfaces/libs/constants/route_paths/auth_route_paths_constant.dart @@ -1,7 +1,7 @@ import "app_route_paths_constant.dart"; /// Sub paths of [AppRoutePaths.auth]. -abstract final class AppRouteAuthPaths { +abstract final class AuthRoutePaths { static const signIn = "/sign-in"; static const signUp = "/sign-up"; } diff --git a/lib/interfaces/libs/constants/app_route_profile_paths_constant.dart b/lib/interfaces/libs/constants/route_paths/profile_route_paths_constant.dart similarity index 71% rename from lib/interfaces/libs/constants/app_route_profile_paths_constant.dart rename to lib/interfaces/libs/constants/route_paths/profile_route_paths_constant.dart index 564a8bc..8a3c650 100644 --- a/lib/interfaces/libs/constants/app_route_profile_paths_constant.dart +++ b/lib/interfaces/libs/constants/route_paths/profile_route_paths_constant.dart @@ -1,6 +1,6 @@ import "app_route_paths_constant.dart"; /// Sub paths of [AppRoutePaths.profile]. -abstract final class AppRouteProfilePaths { +abstract final class ProfileRoutePaths { static const root = ""; } diff --git a/lib/interfaces/libs/constants/app_route_stories_paths_constant.dart b/lib/interfaces/libs/constants/route_paths/stories_route_paths_constant.dart similarity index 81% rename from lib/interfaces/libs/constants/app_route_stories_paths_constant.dart rename to lib/interfaces/libs/constants/route_paths/stories_route_paths_constant.dart index b34bb13..d349516 100644 --- a/lib/interfaces/libs/constants/app_route_stories_paths_constant.dart +++ b/lib/interfaces/libs/constants/route_paths/stories_route_paths_constant.dart @@ -1,7 +1,7 @@ import "app_route_paths_constant.dart"; /// Sub paths of [AppRoutePaths.stories]. -abstract final class AppRouteStoriesPaths { +abstract final class StoriesRoutePaths { static const root = ""; static const post = "post"; static const view$storyId = "view/:storyId"; diff --git a/lib/interfaces/libs/providers/modules/picked_image_provider.dart b/lib/interfaces/libs/providers/modules/picked_image_provider.dart index f14d51d..0ebde0d 100644 --- a/lib/interfaces/libs/providers/modules/picked_image_provider.dart +++ b/lib/interfaces/libs/providers/modules/picked_image_provider.dart @@ -1,11 +1,15 @@ +import "dart:convert"; + import "package:camera/camera.dart"; import "package:flutter/foundation.dart"; import "package:flutter/material.dart"; import "package:image_picker/image_picker.dart"; +import "package:shared_preferences/shared_preferences.dart"; import "package:dicoding_story_fl/interfaces/libs/widgets.dart"; import "package:dicoding_story_fl/libs/constants.dart"; import "package:dicoding_story_fl/libs/extensions.dart"; +import "package:dicoding_story_fl/service_locator.dart"; import "../libs/types.dart"; @@ -15,15 +19,42 @@ import "../libs/types.dart"; final class PickedImageProvider extends AsyncValueNotifier { /// {@macro dicoding_story_fl.interfaces.ux.providers.PickedImageProvider} PickedImageProvider() : super(null) { + cacheService = ServiceLocator.find(); + + final cachedImageBytes = cacheService.getString(cacheKey); + + if (cachedImageBytes != null) { + final imageBytes = jsonDecode(cachedImageBytes) as List; + + value = XFile.fromData( + Uint8List.fromList( + imageBytes.map((e) => e is int ? e : int.parse(e)).toList(), + ), + ); + } + if (defaultTargetPlatform == TargetPlatform.android) { Future.microtask(() => _retrieveLostData()).then((_) => null); } } + @override + void dispose() { + cacheService.remove(cacheKey); + + super.dispose(); + } + + late final SharedPreferences cacheService; + final cacheKey = "providers.picked_image_provider"; + /// Reset [value] to `null`. /// /// Incase you need it. - void resetValue() => value = null; + void resetValue() { + cacheService.remove(cacheKey); + value = null; + } /// Pick image from gallery or camera. /// @@ -47,6 +78,10 @@ final class PickedImageProvider extends AsyncValueNotifier { if (dialogResult == null) return null; + cacheService.setString( + cacheKey, + jsonEncode((await dialogResult.readAsBytes()).toList()), + ); value = dialogResult; return dialogResult; @@ -60,6 +95,10 @@ final class PickedImageProvider extends AsyncValueNotifier { if (image == null) return null; + cacheService.setString( + cacheKey, + jsonEncode((await image.readAsBytes()).toList()), + ); value = image; return image; @@ -84,7 +123,7 @@ final class PickedImageProvider extends AsyncValueNotifier { } } - /// `Android` only. + /// `Android` data recovery. Future _retrieveLostData() async { isLoading = true; diff --git a/lib/interfaces/libs/providers/modules/stories_provider.dart b/lib/interfaces/libs/providers/modules/stories_provider.dart index ecfe5b8..ad6d1c3 100644 --- a/lib/interfaces/libs/providers/modules/stories_provider.dart +++ b/lib/interfaces/libs/providers/modules/stories_provider.dart @@ -31,14 +31,24 @@ final class StoriesProvider extends AsyncValueNotifier> { isLoading = true; try { - final stories = await ServiceLocator.find() - .execute(GetStoriesRequestDto( - page: page, - size: storiesCount, - hasCoordinates: true, - )); - - if (stories.length < storiesCount) { + final getStoriesUseCase = ServiceLocator.find(); + + final halfStoriesCount = storiesCount ~/ 2; + + final List stories = { + ...(await getStoriesUseCase.execute(GetStoriesRequestDto( + page: page, + size: halfStoriesCount, + hasCoordinates: true, + ))), + ...(await getStoriesUseCase.execute(GetStoriesRequestDto( + page: page, + size: halfStoriesCount, + ))), + }.toList() + ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); + + if (stories.length < halfStoriesCount) { _isLatestPage = true; } else { _page++; diff --git a/lib/interfaces/libs/widgets.dart b/lib/interfaces/libs/widgets.dart index 12069cb..32c67ba 100644 --- a/lib/interfaces/libs/widgets.dart +++ b/lib/interfaces/libs/widgets.dart @@ -1,7 +1,7 @@ export "widgets/list_tile/app_about_list_tile.dart"; export "widgets/list_tile/locale_list_tile.dart"; export "widgets/list_tile/theme_list_tile.dart"; -export "widgets/maps/custom_maps_dialog.dart"; +export "widgets/maps/custom_maps_view.dart"; export "widgets/maps/get_location_dialog.dart"; export "widgets/media/common_network_image.dart"; export "widgets/media/custom_camera.dart"; diff --git a/lib/interfaces/libs/widgets/maps/custom_maps_dialog.dart b/lib/interfaces/libs/widgets/maps/custom_maps_dialog.dart deleted file mode 100644 index 0942d2a..0000000 --- a/lib/interfaces/libs/widgets/maps/custom_maps_dialog.dart +++ /dev/null @@ -1,417 +0,0 @@ -import "package:fl_utilities/fl_utilities.dart"; -import "package:flutter/material.dart"; -import "package:google_maps_flutter/google_maps_flutter.dart"; - -import "package:dicoding_story_fl/domain/entities.dart"; -import "package:dicoding_story_fl/interfaces/libs/constants.dart"; -import "package:dicoding_story_fl/interfaces/libs/l10n.dart"; -import "package:dicoding_story_fl/interfaces/libs/widgets.dart"; -import "package:dicoding_story_fl/libs/constants.dart"; -import "package:dicoding_story_fl/service_locator.dart"; -import "package:dicoding_story_fl/use_cases.dart"; - -/// A fullscreen custom Google Maps dialog that return [LocationData] on -/// location confirmation. -class CustomMapsDialog extends StatefulWidget { - const CustomMapsDialog({ - super.key, - this.initialLocation, - this.title, - this.readonly = false, - }); - - final LocationData? initialLocation; - - /// Dialog title. - /// - /// Only used when [readonly] is `false`. - final String? title; - - /// Dialog is readonly and won't return any value on [Navigator.pop]. - /// - /// Useful for displaying [initialLocation]. - final bool readonly; - - @override - State createState() => _CustomMapsDialogState(); -} - -class _CustomMapsDialogState extends State { - LocationData? get initialLocation => widget.initialLocation; - String? get title => widget.title; - bool get readonly => widget.readonly; - - /// Value to return on location confirmation. - LocationData? location; - - /// Act as value store for [FutureBuilder]. - /// - /// For the actual fetch, refer to [_fetchInitLocation]. - late Future _fetchInitLocationValue; - Future _fetchInitLocation() async { - if (initialLocation != null) return initialLocation!; - - return ServiceLocator.find().execute(null); - } - - GoogleMapController? _mapController; - late CameraPosition _mapCam; - - bool _isFetchingMapLocation = true; - - /// Current map location. - /// - /// Updated on camera move, but did'nt trigger widget rebuild. - LocationData get _currentLocation { - final location = _mapCam.target; - - return LocationData(location.latitude, location.longitude); - } - - /// Fetched map location on map camera idle. - /// - /// Used to prevent re-fetching [location] detail on map idle. - late LocationData _fetchedLocation; - - // - - - - - - - - - - - - - - - - - - - - - - - - - - // GOOGLE MAP CALLBACKS - // - - - - - - - - - - - - - - - - - - - - - - - - - - - void _onMapCamMoveStart() { - setState(() => _isFetchingMapLocation = true); - } - - void _onMapCamMove(cam) => _mapCam = cam; - - Future _onMapCamIdle() async { - if (location != null) { - if (_fetchedLocation == _currentLocation) { - setState(() => _isFetchingMapLocation = false); - return; - } - } - - try { - location = await ServiceLocator.find() - .execute(ReverseGeocodingRequestDto( - _currentLocation.latitude, - _currentLocation.longitude, - includeDisplayName: true, - )); - } catch (err, trace) { - kLogger.w( - "Reverse Geocoding Fail", - error: err, - stackTrace: trace, - ); - - location = _currentLocation; - } finally { - setState(() { - _isFetchingMapLocation = false; - _fetchedLocation = _currentLocation; - }); - } - } - - // - - - - - - - - - - - - - - - - - - - - - - - - - - // CUSTOM MAP ACTION - // - - - - - - - - - - - - - - - - - - - - - - - - - - - Future _onLocationTap() async { - final location = - await ServiceLocator.find().execute(null); - - await _mapController?.animateCamera( - CameraUpdate.newLatLng(LatLng(location.latitude, location.longitude)), - ); - } - - void _onMapZoomInTap() => - _mapController?.animateCamera(CameraUpdate.zoomIn()); - - void _onMapZoomOutTap() => - _mapController?.animateCamera(CameraUpdate.zoomOut()); - - /// Move map camera to [initialLocation]. - void _animateMapCameraToInitLocation() { - if (initialLocation == null) return; - - _mapController?.animateCamera( - CameraUpdate.newLatLng( - LatLng(initialLocation!.latitude, initialLocation!.longitude), - ), - ); - } - - // - - - - - - - - - - - - - - - - - - - - - - - - - - // DESCRIBE - // - - - - - - - - - - - - - - - - - - - - - - - - - - - @override - void initState() { - super.initState(); - - _fetchInitLocationValue = _fetchInitLocation(); - } - - @override - void dispose() { - _mapController?.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = context.theme; - final colorScheme = theme.colorScheme; - final textTheme = theme.textTheme; - - final appL10n = AppL10n.of(context)!; - final materialL10n = MaterialLocalizations.of(context); - - return Dialog.fullscreen( - child: Stack( - fit: StackFit.expand, - children: [ - // maps - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Stack( - fit: StackFit.expand, - children: [ - // map - ColoredBox( - color: theme.scaffoldBackgroundColor, - child: FutureBuilder( - future: _fetchInitLocationValue, - builder: (context, snapshot) { - final initLocation = snapshot.data; - - if (initLocation == null) { - if (snapshot.hasError) { - return SizedErrorWidget( - error: snapshot.error, - trace: snapshot.stackTrace, - action: ElevatedButton( - onPressed: () => setState(() { - _fetchInitLocationValue = - _fetchInitLocation(); - }), - child: Text( - materialL10n.refreshIndicatorSemanticLabel, - ), - ), - ); - } - - return const Center( - child: CircularProgressIndicator()); - } - - final initMapCam = CameraPosition( - target: LatLng( - initLocation.latitude, - initLocation.longitude, - ), - zoom: 20, - ); - - return GoogleMap( - initialCameraPosition: initMapCam, - onMapCreated: (controller) => setState(() { - _mapController = controller; - _mapCam = initMapCam; - _fetchedLocation = _currentLocation; - _onMapCamIdle(); - }), - onCameraMoveStarted: _onMapCamMoveStart, - onCameraMove: _onMapCamMove, - onCameraIdle: _onMapCamIdle, - style: context.theme.brightness == Brightness.dark - ? kDarkGoogleMapsStyle - : null, - myLocationButtonEnabled: false, - zoomControlsEnabled: false, - mapToolbarEnabled: false, - ); - }, - ), - ), - - // map marker - if (_mapController != null) ...[ - Center( - child: CircleAvatar( - radius: 4, - backgroundColor: colorScheme.primary, - ), - ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _isFetchingMapLocation - ? SizedBox.square( - dimension: 20.0, - child: CircularProgressIndicator( - color: colorScheme.primary, - ), - ) - : Icon( - Icons.place, - size: 40.0, - color: colorScheme.primary, - ), - const SizedBox(height: 56.0), - ], - ), - ], - ], - ), - ), - - // filler for missing sheet space on shrink. - Container(height: kToolbarHeight, color: theme.cardColor), - ], - ), - - // dialog actions and confirmation sheet - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // dialog actions - Expanded( - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - IconButton.filledTonal( - onPressed: () => - Navigator.pop(context, initialLocation), - icon: const Icon(Icons.close), - tooltip: materialL10n.closeButtonTooltip, - ), - // - - AnimatedCrossFade( - duration: Durations.medium1, - crossFadeState: switch (_mapController) { - null => CrossFadeState.showFirst, - _ => CrossFadeState.showSecond, - }, - firstChild: const SizedBox.shrink(), - secondChild: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (readonly) - IconButton.filledTonal( - onPressed: _animateMapCameraToInitLocation, - icon: const Icon(Icons.restore), - tooltip: appL10n.backToInitialLocation, - ), - if (!readonly) - IconButton.filledTonal( - onPressed: _onLocationTap, - icon: const Icon(Icons.gps_fixed), - tooltip: appL10n.deviceLocation, - ), - if (!kIsNativeMobile) ...[ - const SizedBox(height: 8.0), - IconButton.filledTonal( - onPressed: _onMapZoomInTap, - icon: const Icon(Icons.zoom_in), - tooltip: appL10n.zoomIn, - ), - const SizedBox(height: 8.0), - IconButton.filledTonal( - onPressed: _onMapZoomOutTap, - icon: const Icon(Icons.zoom_out), - tooltip: appL10n.zoomOut, - ), - ], - ], - ), - ), - ], - ), - ), - ), - - // confirmation sheet - AnimatedSize( - duration: Durations.medium1, - curve: Curves.easeInOutSine, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 16.0), - decoration: BoxDecoration( - color: theme.cardColor, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(16.0), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // title - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Text( - readonly - ? title ?? appL10n.location - : appL10n.setLocation, - style: textTheme.headlineSmall, - ), - ), - const SizedBox(height: 8.0), - - // location details - if (_isFetchingMapLocation) - const Center(child: CircularProgressIndicator()), - if (!_isFetchingMapLocation) ...[ - ListTile( - leading: const Icon(Icons.place_outlined), - title: Text( - location?.displayName ?? - location?.latLon ?? - _currentLocation.latLon, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: location != null - ? Text( - location?.address ?? - _currentLocation.address ?? - "-", - maxLines: 2, - overflow: TextOverflow.ellipsis, - ) - : null, - ), - if (!readonly) ...[ - const SizedBox(height: 8.0), - Align( - alignment: Alignment.center, - child: FilledButton( - onPressed: () => Navigator.pop(context, location), - child: Text(appL10n.confirm), - ), - ) - ] - ], - ], - ), - ), - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/interfaces/libs/widgets/maps/custom_maps_view.dart b/lib/interfaces/libs/widgets/maps/custom_maps_view.dart new file mode 100644 index 0000000..51417d6 --- /dev/null +++ b/lib/interfaces/libs/widgets/maps/custom_maps_view.dart @@ -0,0 +1,437 @@ +import "package:fl_utilities/fl_utilities.dart"; +import "package:flutter/material.dart"; +import "package:google_maps_flutter/google_maps_flutter.dart"; + +import "package:dicoding_story_fl/domain/entities.dart"; +import "package:dicoding_story_fl/interfaces/libs/constants.dart"; +import "package:dicoding_story_fl/interfaces/libs/l10n.dart"; +import "package:dicoding_story_fl/interfaces/libs/widgets.dart"; +import "package:dicoding_story_fl/libs/constants.dart"; +import "package:dicoding_story_fl/service_locator.dart"; +import "package:dicoding_story_fl/use_cases.dart"; + +/// A custom Google Maps view that return [LocationData] on location +/// confirmation. +/// +/// > WARNING: This widget is build on [Stack] and did not have any background. +/// We recommend to use it on [Dialog.fullscreen] or [Scaffold.body] with +/// [onConfirm]. +class CustomMapsView extends StatefulWidget { + const CustomMapsView({ + super.key, + this.initialLocation, + this.title, + this.readonly = false, + this.closeButton, + this.onConfirm, + }); + + final LocationData? initialLocation; + + /// View bottom sheet title. + final String? title; + + /// View is readonly and won't return any value on [Navigator.pop]. + /// + /// Useful for displaying [initialLocation]. + final bool readonly; + + /// Override default close button. + /// + /// Typically a [IconButton.filledTonal]. + final Widget? closeButton; + + /// Override on location confirmation default behavior. + /// + /// Default behavior is [Navigator.pop] with [LocationData] as result. + final CustomMapsViewConfirmationCallback? onConfirm; + + @override + State createState() => _CustomMapsViewState(); +} + +typedef CustomMapsViewConfirmationCallback = void Function( + LocationData location, +); + +class _CustomMapsViewState extends State { + LocationData? get initialLocation => widget.initialLocation; + String? get title => widget.title; + bool get readonly => widget.readonly; + Widget? get closeButton => widget.closeButton; + CustomMapsViewConfirmationCallback? get onConfirm => widget.onConfirm; + + /// Value to return on location confirmation. + LocationData? location; + + /// Act as value store for [FutureBuilder]. + /// + /// For the actual fetch, refer to [_fetchInitLocation]. + late Future _fetchInitLocationValue; + Future _fetchInitLocation() async { + if (initialLocation != null) return initialLocation!; + + return ServiceLocator.find().execute(null); + } + + GoogleMapController? _mapController; + late CameraPosition _mapCam; + + bool _isFetchingMapLocation = true; + + /// Current map location. + /// + /// Updated on camera move, but did'nt trigger widget rebuild. + LocationData get _currentLocation { + final location = _mapCam.target; + + return LocationData(location.latitude, location.longitude); + } + + /// Fetched map location on map camera idle. + /// + /// Used to prevent re-fetching [location] detail on map idle. + late LocationData _fetchedLocation; + + // - - - - - - - - - - - - - - - - - - - - - - - - - + // GOOGLE MAP CALLBACKS + // - - - - - - - - - - - - - - - - - - - - - - - - - + + void _onMapCamMoveStart() { + setState(() => _isFetchingMapLocation = true); + } + + void _onMapCamMove(cam) => _mapCam = cam; + + Future _onMapCamIdle() async { + if (location != null) { + if (_fetchedLocation == _currentLocation) { + setState(() => _isFetchingMapLocation = false); + return; + } + } + + try { + location = await ServiceLocator.find() + .execute(ReverseGeocodingRequestDto( + _currentLocation.latitude, + _currentLocation.longitude, + includeDisplayName: true, + )); + } catch (err, trace) { + kLogger.w( + "Reverse Geocoding Fail", + error: err, + stackTrace: trace, + ); + + location = _currentLocation; + } finally { + setState(() { + _isFetchingMapLocation = false; + _fetchedLocation = _currentLocation; + }); + } + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - + // CUSTOM MAP ACTION + // - - - - - - - - - - - - - - - - - - - - - - - - - + + Future _onLocationTap() async { + final location = + await ServiceLocator.find().execute(null); + + await _mapController?.animateCamera( + CameraUpdate.newLatLng(LatLng(location.latitude, location.longitude)), + ); + } + + void _onMapZoomInTap() => + _mapController?.animateCamera(CameraUpdate.zoomIn()); + + void _onMapZoomOutTap() => + _mapController?.animateCamera(CameraUpdate.zoomOut()); + + /// Move map camera to [initialLocation]. + void _animateMapCameraToInitLocation() { + if (initialLocation == null) return; + + _mapController?.animateCamera( + CameraUpdate.newLatLng( + LatLng(initialLocation!.latitude, initialLocation!.longitude), + ), + ); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - + // DESCRIBE + // - - - - - - - - - - - - - - - - - - - - - - - - - + + @override + void initState() { + super.initState(); + + _fetchInitLocationValue = _fetchInitLocation(); + } + + @override + void dispose() { + _mapController?.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = context.theme; + final colorScheme = theme.colorScheme; + final textTheme = theme.textTheme; + + final appL10n = AppL10n.of(context)!; + final materialL10n = MaterialLocalizations.of(context); + + return Stack( + fit: StackFit.expand, + children: [ + // maps + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Stack( + fit: StackFit.expand, + children: [ + // map + ColoredBox( + color: theme.scaffoldBackgroundColor, + child: FutureBuilder( + future: _fetchInitLocationValue, + builder: (context, snapshot) { + final initLocation = snapshot.data; + + if (initLocation == null) { + if (snapshot.hasError) { + return SizedErrorWidget( + error: snapshot.error, + trace: snapshot.stackTrace, + action: ElevatedButton( + onPressed: () => setState(() { + _fetchInitLocationValue = + _fetchInitLocation(); + }), + child: Text( + materialL10n.refreshIndicatorSemanticLabel, + ), + ), + ); + } + + return const Center( + child: CircularProgressIndicator()); + } + + final initMapCam = CameraPosition( + target: LatLng( + initLocation.latitude, + initLocation.longitude, + ), + zoom: 20, + ); + + return GoogleMap( + initialCameraPosition: initMapCam, + onMapCreated: (controller) => setState(() { + _mapController = controller; + _mapCam = initMapCam; + _fetchedLocation = _currentLocation; + _onMapCamIdle(); + }), + onCameraMoveStarted: _onMapCamMoveStart, + onCameraMove: _onMapCamMove, + onCameraIdle: _onMapCamIdle, + style: context.theme.brightness == Brightness.dark + ? kDarkGoogleMapsStyle + : null, + myLocationButtonEnabled: false, + zoomControlsEnabled: false, + mapToolbarEnabled: false, + ); + }, + ), + ), + + // map marker + if (_mapController != null) ...[ + Center( + child: CircleAvatar( + radius: 4, + backgroundColor: colorScheme.primary, + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _isFetchingMapLocation + ? SizedBox.square( + dimension: 20.0, + child: CircularProgressIndicator( + color: colorScheme.primary, + ), + ) + : Icon( + Icons.place, + size: 40.0, + color: colorScheme.primary, + ), + const SizedBox(height: 56.0), + ], + ), + ], + ], + ), + ), + + // filler for missing sheet space on shrink. + Container(height: kToolbarHeight, color: theme.cardColor), + ], + ), + + // dialog actions and confirmation sheet + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // dialog actions + Expanded( + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + closeButton ?? + IconButton.filledTonal( + onPressed: () => + Navigator.pop(context, initialLocation), + icon: const Icon(Icons.close), + tooltip: materialL10n.closeButtonTooltip, + ), + // + + AnimatedCrossFade( + duration: Durations.medium1, + crossFadeState: switch (_mapController) { + null => CrossFadeState.showFirst, + _ => CrossFadeState.showSecond, + }, + firstChild: const SizedBox.shrink(), + secondChild: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (readonly) + IconButton.filledTonal( + onPressed: _animateMapCameraToInitLocation, + icon: const Icon(Icons.restore), + tooltip: appL10n.backToInitialLocation, + ), + if (!readonly) + IconButton.filledTonal( + onPressed: _onLocationTap, + icon: const Icon(Icons.gps_fixed), + tooltip: appL10n.deviceLocation, + ), + if (!kIsNativeMobile) ...[ + const SizedBox(height: 8.0), + IconButton.filledTonal( + onPressed: _onMapZoomInTap, + icon: const Icon(Icons.zoom_in), + tooltip: appL10n.zoomIn, + ), + const SizedBox(height: 8.0), + IconButton.filledTonal( + onPressed: _onMapZoomOutTap, + icon: const Icon(Icons.zoom_out), + tooltip: appL10n.zoomOut, + ), + ], + ], + ), + ), + ], + ), + ), + ), + + // confirmation sheet + AnimatedSize( + duration: Durations.medium1, + curve: Curves.easeInOutSine, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16.0), + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16.0), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // title + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + title ?? + (readonly ? appL10n.location : appL10n.setLocation), + style: textTheme.headlineSmall, + ), + ), + const SizedBox(height: 8.0), + + // location details + if (_isFetchingMapLocation) + const Center(child: CircularProgressIndicator()), + if (!_isFetchingMapLocation) ...[ + ListTile( + leading: const Icon(Icons.place_outlined), + title: Text( + location?.displayName ?? + location?.latLon ?? + _currentLocation.latLon, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: location != null + ? Text( + location?.address ?? + _currentLocation.address ?? + "-", + maxLines: 2, + overflow: TextOverflow.ellipsis, + ) + : null, + ), + if (!readonly) ...[ + const SizedBox(height: 8.0), + Align( + alignment: Alignment.center, + child: FilledButton( + onPressed: onConfirm == null + ? () => Navigator.pop(context, location) + : () => onConfirm!(location!), + child: Text(appL10n.confirm), + ), + ) + ] + ], + ], + ), + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/interfaces/libs/widgets/maps/get_location_dialog.dart b/lib/interfaces/libs/widgets/maps/get_location_dialog.dart index b1a6b39..5f1fe7e 100644 --- a/lib/interfaces/libs/widgets/maps/get_location_dialog.dart +++ b/lib/interfaces/libs/widgets/maps/get_location_dialog.dart @@ -10,23 +10,6 @@ import "package:dicoding_story_fl/service_locator.dart"; import "package:dicoding_story_fl/use_cases.dart"; /// A fullscreen dialog that return [LocationData] on location selection. -/// -/// Usecase: -/// ```dart -/// Builder(builder: (context) { -/// return ElevatedButton( -/// onPressed: () async { -/// final locationResult = await showDialog( -/// context: context, -/// builder: (_) => const PickLocationDialog(), -/// ) -/// -/// debugPrint('${locationResult?.address}'); -/// }, -/// child: const Text('Set Location'), -/// ); -/// }); -/// ``` class GetLocationDialog extends StatefulWidget { const GetLocationDialog({super.key, this.initialLocation}); @@ -39,6 +22,87 @@ class GetLocationDialog extends StatefulWidget { class _GetLocationDialogState extends State { LocationData? get initialLocation => widget.initialLocation; + late final PageController pageController; + + Duration get _pageAnimationDuration => Durations.long1; + Curve get _pageAnimationCurve => Curves.fastOutSlowIn; + + @override + void initState() { + super.initState(); + + pageController = PageController(); + } + + @override + void dispose() { + pageController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Dialog.fullscreen( + child: PageView( + controller: pageController, + physics: const NeverScrollableScrollPhysics(), + children: [ + _GetLocationDialogSearchView( + initialLocation: initialLocation, + actions: [ + Builder(builder: (context) { + return FilledButton.tonalIcon( + onPressed: () => pageController.animateToPage( + 1, + duration: _pageAnimationDuration, + curve: _pageAnimationCurve, + ), + icon: const Icon(Icons.map_outlined), + label: Text(AppL10n.of(context)!.setFromMap), + ); + }), + ], + ), + CustomMapsView( + initialLocation: initialLocation, + closeButton: Builder(builder: (context) { + return IconButton.filledTonal( + onPressed: () => pageController.animateToPage( + 0, + duration: _pageAnimationDuration, + curve: _pageAnimationCurve, + ), + icon: const Icon(Icons.arrow_back), + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + ); + }), + ), + ], + ), + ); + } +} + +class _GetLocationDialogSearchView extends StatefulWidget { + const _GetLocationDialogSearchView({ + this.initialLocation, + this.actions = const [], + }); + + final LocationData? initialLocation; + final List actions; + + @override + State<_GetLocationDialogSearchView> createState() => + _GetLocationDialogSearchViewState(); +} + +class _GetLocationDialogSearchViewState + extends State<_GetLocationDialogSearchView> { + LocationData? get initialLocation => widget.initialLocation; + List get actions => widget.actions; + /// Indicate if the dialog is searching a place. bool _isSearching = false; final List _searchResults = []; @@ -102,105 +166,82 @@ class _GetLocationDialogState extends State { Widget build(BuildContext context) { final appL10n = AppL10n.of(context)!; - return Dialog.fullscreen( - child: Column( - children: [ - AppBar( - leading: CloseButton( - onPressed: () => Navigator.pop(context, initialLocation), - ), - title: Text(appL10n.setLocation), + return Column( + children: [ + AppBar( + leading: CloseButton( + onPressed: () => Navigator.pop(context, initialLocation), ), - SizedBox( - width: double.infinity, - child: Align( - alignment: Alignment.center, - child: Container( - width: 800.0, - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextField( - controller: _searchController, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.search), - hintText: appL10n.searchLocationHint, - ), - onChanged: (String text) async { - if (text.length < 3) return; - - await _handleSearchPlace(text); - }.debounce(delay: Durations.long2), + title: Text(appL10n.setLocation), + ), + SizedBox( + width: double.infinity, + child: Align( + alignment: Alignment.center, + child: Container( + width: 800.0, + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: _searchController, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search), + hintText: appL10n.searchLocationHint, ), - const SizedBox(height: 8.0), - Builder(builder: (context) { - return FilledButton.tonalIcon( - onPressed: () async { - final customMapsDialogResult = - await showDialog( - context: context, - builder: (_) => CustomMapsDialog( - initialLocation: initialLocation, - ), - ); - - if (customMapsDialogResult != null) return; - if (customMapsDialogResult == initialLocation) return; - - if (context.mounted) { - Navigator.pop(context, customMapsDialogResult); - } - }, - icon: const Icon(Icons.map_outlined), - label: Text(appL10n.setFromMap), - ); - }), - ], - ), + onChanged: (String text) async { + if (text.length < 3) return; + + await _handleSearchPlace(text); + }.debounce(delay: Durations.long2), + ), + if (actions.isNotEmpty) const SizedBox(height: 8.0), + ...actions, + ], ), ), ), - Expanded( - child: _isSearching - ? const SizedBox( - child: Center(child: CircularProgressIndicator()), - ) - : _hasSearchedPlace && _searchResults.isEmpty - ? Padding( - padding: const EdgeInsets.all(16.0).copyWith(top: 0), - child: SizedErrorWidget( - error: _searchError ?? - AppException( - name: appL10n.searchLocationErrorTitle, - message: appL10n.searchLocationErrorMessage, - ), - ), - ) - : ListView.builder( - padding: const EdgeInsets.only(bottom: 16.0), - itemCount: _searchResults.length, - itemBuilder: (context, index) { - final location = _searchResults[index]; - - return ListTile( - leading: const Icon(Icons.place_outlined), - title: Text( - location.displayName ?? location.latLon, + ), + Expanded( + child: _isSearching + ? const SizedBox( + child: Center(child: CircularProgressIndicator()), + ) + : _hasSearchedPlace && _searchResults.isEmpty + ? Padding( + padding: const EdgeInsets.all(16.0).copyWith(top: 0), + child: SizedErrorWidget( + error: _searchError ?? + AppException( + name: appL10n.searchLocationErrorTitle, + message: appL10n.searchLocationErrorMessage, ), - subtitle: Text( - location.address ?? "-", - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - onTap: () => Navigator.pop(context, location), - ); - }, ), - ), - ], - ), + ) + : ListView.builder( + padding: const EdgeInsets.only(bottom: 16.0), + itemCount: _searchResults.length, + itemBuilder: (context, index) { + final location = _searchResults[index]; + + return ListTile( + leading: const Icon(Icons.place_outlined), + title: Text( + location.displayName ?? location.latLon, + ), + subtitle: Text( + location.address ?? "-", + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + onTap: () => Navigator.pop(context, location), + ); + }, + ), + ), + ], ); } } diff --git a/lib/interfaces/modules/routes/auth/sign_in_route.dart b/lib/interfaces/modules/routes/auth/sign_in_route.dart index 791819b..930d10c 100644 --- a/lib/interfaces/modules/routes/auth/sign_in_route.dart +++ b/lib/interfaces/modules/routes/auth/sign_in_route.dart @@ -13,7 +13,7 @@ import "package:dicoding_story_fl/libs/extensions.dart"; /// [SignInRoute] build decorator. const signInRouteBuild = TypedGoRoute( - path: "${AppRoutePaths.auth}${AppRouteAuthPaths.signIn}", + path: "${AppRoutePaths.auth}${AuthRoutePaths.signIn}", ); final class SignInRoute extends GoRouteData { diff --git a/lib/interfaces/modules/routes/auth/sign_up_route.dart b/lib/interfaces/modules/routes/auth/sign_up_route.dart index 162f9d6..37c5dd6 100644 --- a/lib/interfaces/modules/routes/auth/sign_up_route.dart +++ b/lib/interfaces/modules/routes/auth/sign_up_route.dart @@ -13,7 +13,7 @@ import "package:dicoding_story_fl/libs/extensions.dart"; /// [SignUpRoute] build decorator. const signUpRouteBuild = TypedGoRoute( - path: "${AppRoutePaths.auth}${AppRouteAuthPaths.signUp}", + path: "${AppRoutePaths.auth}${AuthRoutePaths.signUp}", ); final class SignUpRoute extends GoRouteData { diff --git a/lib/interfaces/modules/routes/root/stories/routes/post_story_route.dart b/lib/interfaces/modules/routes/root/stories/routes/post_story_route.dart index 2851f31..af08b6a 100644 --- a/lib/interfaces/modules/routes/root/stories/routes/post_story_route.dart +++ b/lib/interfaces/modules/routes/root/stories/routes/post_story_route.dart @@ -1,4 +1,5 @@ import "package:dicoding_story_fl/domain/entities.dart"; +import "package:dicoding_story_fl/interfaces/modules.dart"; import "package:fl_utilities/fl_utilities.dart"; import "package:flex_color_scheme/flex_color_scheme.dart"; import "package:flutter/material.dart"; @@ -16,23 +17,27 @@ import "package:dicoding_story_fl/libs/extensions.dart"; /// [PostStoryRoute] build decorator. const postStoryRouteBuild = TypedGoRoute( - path: AppRouteStoriesPaths.post, + path: StoriesRoutePaths.post, ); final class PostStoryRoute extends GoRouteData { - const PostStoryRoute(); + const PostStoryRoute([this.description]); + + final String? description; @override Widget build(BuildContext context, GoRouterState state) { return ChangeNotifierProvider.value( value: PickedImageProvider(), - child: const _PostStoryRouteScreen(), + child: _PostStoryRouteScreen(description), ); } } class _PostStoryRouteScreen extends StatefulWidget { - const _PostStoryRouteScreen(); + const _PostStoryRouteScreen([this.description]); + + final String? description; @override State<_PostStoryRouteScreen> createState() => _PostStoryRouteScreenState(); @@ -161,7 +166,7 @@ class _PostStoryRouteScreenState extends State<_PostStoryRouteScreen> { super.initState(); _formKey = GlobalKey(); - _descriptionController = TextEditingController(); + _descriptionController = TextEditingController(text: widget.description); } @override @@ -177,7 +182,20 @@ class _PostStoryRouteScreenState extends State<_PostStoryRouteScreen> { final appL10n = AppL10n.of(context)!; return Scaffold( - appBar: AppBar(title: Text(appL10n.postStory)), + appBar: AppBar( + leading: Builder(builder: (context) { + return BackButton( + onPressed: () { + // provider dispose handled here since widget dispose cause an + // error. + context.read().dispose(); + + Navigator.pop(context); + }, + ); + }), + title: Text(appL10n.postStory), + ), body: Builder( builder: (context) { final pickedImageProv = context.watch(); @@ -263,6 +281,9 @@ class _PostStoryRouteScreenState extends State<_PostStoryRouteScreen> { formKey: _formKey, descController: _descriptionController, descIsEnabled: !isLoading, + onDescChanged: (value) { + PostStoryRoute(value).go(context); + }.debounce(), address: _locationData?.displayName ?? _locationData?.address ?? _locationData?.latLon, @@ -304,6 +325,7 @@ class _PostStoryFormDelegate { this.formKey, this.descController, this.descIsEnabled, + this.onDescChanged, this.address, this.onPostButtonTap, this.onAddressSectionTap, @@ -315,6 +337,7 @@ class _PostStoryFormDelegate { final Key? formKey; final TextEditingController? descController; final bool? descIsEnabled; + final ValueChanged? onDescChanged; final String? address; final VoidCallback? onPostButtonTap; @@ -377,6 +400,7 @@ abstract base class _PostStoryFormBase extends StatelessWidget { keyboardType: TextInputType.multiline, maxLines: null, decoration: InputDecoration(hintText: "${appL10n.tellUsYourStory}..."), + onChanged: delegate.onDescChanged, validator: (text) { if (text == null || text.isEmpty) return appL10n.cannotBeEmpty; diff --git a/lib/interfaces/modules/routes/root/stories/routes/story_detail_route.dart b/lib/interfaces/modules/routes/root/stories/routes/story_detail_route.dart index 4e2a4e7..1bf1743 100644 --- a/lib/interfaces/modules/routes/root/stories/routes/story_detail_route.dart +++ b/lib/interfaces/modules/routes/root/stories/routes/story_detail_route.dart @@ -10,7 +10,7 @@ import "package:dicoding_story_fl/interfaces/libs/providers.dart"; import "package:dicoding_story_fl/interfaces/libs/widgets.dart"; const storyDetailRouteBuild = TypedGoRoute( - path: AppRouteStoriesPaths.view$storyId, + path: StoriesRoutePaths.view$storyId, ); final class StoryDetailRoute extends GoRouteData { @@ -116,9 +116,11 @@ mixin _StoryDetailRouteScreenHelperMixin { showDialog( context: context, builder: (_) { - return CustomMapsDialog( - initialLocation: initialLocation, - readonly: true, + return Dialog.fullscreen( + child: CustomMapsView( + initialLocation: initialLocation, + readonly: true, + ), ); }, ); diff --git a/lib/interfaces/modules/routes/root/stories/stories_route.dart b/lib/interfaces/modules/routes/root/stories/stories_route.dart index 9403c33..8e66c2d 100644 --- a/lib/interfaces/modules/routes/root/stories/stories_route.dart +++ b/lib/interfaces/modules/routes/root/stories/stories_route.dart @@ -19,7 +19,7 @@ export "routes/story_detail_route.dart"; /// [StoriesRoute] build decorator. const storiesRouteBuild = TypedGoRoute( - path: "${AppRoutePaths.stories}${AppRouteStoriesPaths.root}", + path: "${AppRoutePaths.stories}${StoriesRoutePaths.root}", routes: [postStoryRouteBuild, storyDetailRouteBuild], ); @@ -59,7 +59,7 @@ class _StoriesRouteScreenState extends State<_StoriesRouteScreen> { final scrollPosition = _scrollController.position.pixels; final maxScrollPosition = _scrollController.position.maxScrollExtent; - if (scrollPosition >= maxScrollPosition) { + if (scrollPosition >= maxScrollPosition - 600.0) { Future.microtask(_handleFetchStories); } }); diff --git a/lib/use_cases/modules/stories/get_stories_use_case.dart b/lib/use_cases/modules/stories/get_stories_use_case.dart index 9793325..dfa5cad 100644 --- a/lib/use_cases/modules/stories/get_stories_use_case.dart +++ b/lib/use_cases/modules/stories/get_stories_use_case.dart @@ -43,7 +43,7 @@ final class GetStoriesUseCase stackTrace: exception.trace, ); - return e.copyWith(location: LocationData(e.lat!, e.lon!)); + return e.copyWith(location: null); } })); } diff --git a/lib/use_cases/modules/stories/get_story_by_id_use_case.dart b/lib/use_cases/modules/stories/get_story_by_id_use_case.dart index 2ac507b..c089682 100644 --- a/lib/use_cases/modules/stories/get_story_by_id_use_case.dart +++ b/lib/use_cases/modules/stories/get_story_by_id_use_case.dart @@ -1,6 +1,4 @@ import "package:dicoding_story_fl/domain/services.dart"; -import "package:dicoding_story_fl/libs/constants.dart"; -import "package:dicoding_story_fl/libs/extensions.dart"; import "package:injectable/injectable.dart"; import "package:dicoding_story_fl/domain/entities.dart"; @@ -26,19 +24,11 @@ final class GetStoryByIdUseCase implements UseCase { ); } - Future _loadAddress(double latitude, double longitude) async { + Future _loadAddress(double latitude, double longitude) async { try { return await _mapsService.reverseGeocoding(latitude, longitude); - } catch (error, trace) { - final exception = error.toAppException(trace: trace); - - kLogger.w( - "fail to load story address", - error: exception, - stackTrace: exception.trace, - ); - - return LocationData(latitude, longitude); + } catch (error) { + return null; } } }