From 56993ee759277d1111f51b36405a753198ea3749 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 8 Sep 2023 23:48:34 +0200 Subject: [PATCH 01/15] refactor: Use new LocationFetchers.dart to fetch locations --- lib/screens/LocationsOverviewScreen.dart | 119 ++++++++++-------- .../LocationFetchers.dart | 50 ++++++++ .../location_fetcher_service/Fetcher.dart | 104 +++++++++++++++ .../location_fetcher_service/Locations.dart | 16 +++ 4 files changed, 236 insertions(+), 53 deletions(-) create mode 100644 lib/screens/locations_overview_screen_widgets/LocationFetchers.dart create mode 100644 lib/services/location_fetcher_service/Fetcher.dart create mode 100644 lib/services/location_fetcher_service/Locations.dart diff --git a/lib/screens/LocationsOverviewScreen.dart b/lib/screens/LocationsOverviewScreen.dart index bee4eeed..d6a3e9e7 100644 --- a/lib/screens/LocationsOverviewScreen.dart +++ b/lib/screens/LocationsOverviewScreen.dart @@ -23,10 +23,10 @@ import 'package:locus/screens/ImportTaskSheet.dart'; import 'package:locus/screens/SettingsScreen.dart'; import 'package:locus/screens/SharesOverviewScreen.dart'; import 'package:locus/screens/locations_overview_screen_widgets/ActiveSharesSheet.dart'; +import 'package:locus/screens/locations_overview_screen_widgets/LocationFetchers.dart'; import 'package:locus/screens/locations_overview_screen_widgets/OutOfBoundMarker.dart'; import 'package:locus/screens/locations_overview_screen_widgets/ShareLocationSheet.dart'; import 'package:locus/screens/locations_overview_screen_widgets/ViewLocationPopup.dart'; -import 'package:locus/screens/locations_overview_screen_widgets/view_location_fetcher.dart'; import 'package:locus/services/manager_service/background_locator.dart'; import 'package:locus/services/manager_service/helpers.dart'; import 'package:locus/services/settings_service/SettingsMapLocation.dart'; @@ -89,7 +89,7 @@ class _LocationsOverviewScreenState extends State AutomaticKeepAliveClientMixin, WidgetsBindingObserver, TickerProviderStateMixin { - late final ViewLocationFetcher _fetchers; + final _fetchers = LocationFetchers()..enableLocationsUpdates(); MapController? flutterMapController; PopupController? flutterMapPopupController; apple_maps.AppleMapController? appleMapController; @@ -143,7 +143,6 @@ class _LocationsOverviewScreenState extends State final settings = context.read(); final appUpdateService = context.read(); - _createLocationFetcher(); _handleViewAlarmChecker(); _handleNotifications(); @@ -152,25 +151,30 @@ class _LocationsOverviewScreenState extends State WidgetsBinding.instance ..addObserver(this) - ..addPostFrameCallback((_) async { + ..addPostFrameCallback((_) { _setLocationFromSettings(); _updateBackgroundListeners(); initQuickActions(context); _initUniLinks(); _updateLocaleToSettings(); _showUpdateDialogIfRequired(); + _initFetchers(); taskService.checkup(logService); - _fetchers.addListener(_rebuild); appUpdateService.addListener(_rebuild); viewService.addListener(_handleViewServiceChange); - updateCurrentPosition( - askPermissions: false, - showErrorMessage: false, - goToPosition: true, - ); + Geolocator.checkPermission().then((status) { + if ({LocationPermission.always, LocationPermission.whileInUse} + .contains(status)) { + updateCurrentPosition( + askPermissions: false, + showErrorMessage: false, + goToPosition: true, + ); + } + }); }); if (settings.getMapProvider() == MapProvider.openStreetMap) { @@ -224,11 +228,21 @@ class _LocationsOverviewScreenState extends State } } + void _initFetchers() { + final viewService = context.read(); + + _fetchers.addListener(_rebuild); + + _fetchers.addAll(viewService.views); + _fetchers.fetchPreviewLocations(); + } + void _handleViewServiceChange() { final viewService = context.read(); final newView = viewService.views.last; - _fetchers.addView(newView); + _fetchers.add(newView); + _fetchers.fetchPreviewLocations(); } void _setLocationFromSettings() async { @@ -253,10 +267,12 @@ class _LocationsOverviewScreenState extends State ); } - List mergeLocationsIfRequired( - final TaskView view, - ) { - final locations = _fetchers.locations[view] ?? []; + List mergeLocationsIfRequired(final TaskView view) { + final locations = _fetchers.getLocations(view); + + if (locations.isEmpty) { + return locations; + } if (showDetailedLocations && !disableShowDetailedLocations) { return locations; @@ -271,15 +287,11 @@ class _LocationsOverviewScreenState extends State distanceThreshold: LOCATION_MERGE_DISTANCE_THRESHOLD, ); - _cachedMergedLocations[view] = mergedLocations; - return mergedLocations; - } - void _createLocationFetcher() { - final viewService = context.read(); + _cachedMergedLocations[view] = mergedLocations; - _fetchers = ViewLocationFetcher(viewService.views)..fetchLocations(); + return mergedLocations; } void _rebuild() { @@ -776,16 +788,16 @@ class _LocationsOverviewScreenState extends State .expand((element) => element) .toSet(), polylines: Set.from( - _fetchers.locations.entries - .where((entry) => - selectedViewID == null || entry.key.id == selectedViewID) + _fetchers.fetchers + .where((fetcher) => + selectedViewID == null || fetcher.view.id == selectedViewID) .map( - (entry) { - final view = entry.key; + (fetcher) { + final view = fetcher.view; return apple_maps.Polyline( polylineId: apple_maps.PolylineId(view.id), - color: entry.key.color.withOpacity(0.9), + color: view.color.withOpacity(0.9), width: 10, jointType: apple_maps.JointType.round, polylineCap: apple_maps.Cap.roundCap, @@ -796,7 +808,8 @@ class _LocationsOverviewScreenState extends State selectedViewID = view.id; }); }, - points: mergeLocationsIfRequired(entry.key) + // TODO + points: mergeLocationsIfRequired(view) .reversed .map( (location) => apple_maps.LatLng( @@ -845,13 +858,13 @@ class _LocationsOverviewScreenState extends State ), PolylineLayer( polylines: List.from( - _fetchers.locations.entries - .where((entry) => - selectedViewID == null || entry.key.id == selectedViewID) + _fetchers.fetchers + .where((fetcher) => + selectedViewID == null || fetcher.view.id == selectedViewID) .map( - (entry) { - final view = entry.key; - final locations = mergeLocationsIfRequired(entry.key); + (fetcher) { + final view = fetcher.view; + final locations = mergeLocationsIfRequired(view); return Polyline( color: view.color.withOpacity(0.9), @@ -901,9 +914,9 @@ class _LocationsOverviewScreenState extends State markers: viewService.views .where((view) => (selectedViewID == null || view.id == selectedViewID) && - _fetchers.locations[view]?.last != null) + _fetchers.getLocations(view).isNotEmpty) .map((view) { - final latestLocation = _fetchers.locations[view]!.last; + final latestLocation = _fetchers.getLocations(view).last; return Marker( key: Key(view.id), @@ -934,17 +947,17 @@ class _LocationsOverviewScreenState extends State Widget buildOutOfBoundsMarkers() { return Stack( - children: _fetchers.views - .where((view) => - (_fetchers.locations[view]?.isNotEmpty ?? false) && - (selectedViewID == null || selectedViewID == view.id)) + children: _fetchers.fetchers + .where((fetcher) => + (selectedViewID == null || fetcher.view.id == selectedViewID) && + fetcher.locations.isNotEmpty) .map( - (view) => OutOfBoundMarker( - lastViewLocation: _fetchers.locations[view]!.last, + (fetcher) => OutOfBoundMarker( + lastViewLocation: fetcher.locations.last, onTap: () { - showViewLocations(view); + showViewLocations(fetcher.view); }, - view: view, + view: fetcher.view, updateStream: mapEventStream.stream, appleMapController: appleMapController, flutterMapController: flutterMapController, @@ -965,12 +978,13 @@ class _LocationsOverviewScreenState extends State return; } - final latestLocation = _fetchers.locations[view]?.last; + final locations = _fetchers.getLocations(view); - if (latestLocation == null) { + if (locations.isEmpty) { return; } + final latestLocation = locations.last; if (flutterMapController != null) { flutterMapController!.move( LatLng(latestLocation.latitude, latestLocation.longitude), @@ -1208,15 +1222,12 @@ class _LocationsOverviewScreenState extends State return null; } - if (_fetchers.locations[selectedView!] == null) { - return null; - } - - if (_fetchers.locations[selectedView!]!.isEmpty) { + final locations = _fetchers.getLocations(selectedView!); + if (locations.isEmpty) { return null; } - return _fetchers.locations[selectedView!]!.last; + return locations.last; } void importLocation() { @@ -1434,7 +1445,9 @@ class _LocationsOverviewScreenState extends State buildMapActions(), ViewDetailsSheet( view: selectedView, - locations: _fetchers.locations[selectedView], + locations: selectedViewID == null + ? [] + : _fetchers.getLocations(selectedView!), onGoToPosition: (position) { if (flutterMapController != null) { flutterMapController! diff --git a/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart b/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart new file mode 100644 index 00000000..392e0c1b --- /dev/null +++ b/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart @@ -0,0 +1,50 @@ +import 'dart:collection'; + +import 'package:flutter/foundation.dart'; +import 'package:locus/services/location_fetcher_service/Fetcher.dart'; +import 'package:locus/services/location_point_service.dart'; +import 'package:locus/services/view_service.dart'; + +class LocationFetchers extends ChangeNotifier { + final Set _fetchers = {}; + + UnmodifiableSetView get fetchers => UnmodifiableSetView(_fetchers); + + LocationFetchers(); + + void enableLocationsUpdates() { + for (final fetcher in _fetchers) { + fetcher.addListener(notifyListeners); + } + } + + void add(final TaskView view) { + if (_fetchers.any((fetcher) => fetcher.view == view)) { + return; + } + + _fetchers.add(Fetcher(view)); + } + + void addAll(final List views) { + for (final view in views) { + add(view); + } + } + + void fetchPreviewLocations() { + for (final fetcher in _fetchers) { + if (!fetcher.hasFetchedPreviewLocations) { + fetcher.fetchPreviewLocations(); + } + } + } + + Fetcher _findFetcher(final TaskView view) { + return _fetchers.firstWhere((fetcher) => fetcher.view == view); + } + + List getLocations(final TaskView view) { + return _findFetcher(view).locations; + } +} diff --git a/lib/services/location_fetcher_service/Fetcher.dart b/lib/services/location_fetcher_service/Fetcher.dart new file mode 100644 index 00000000..36435248 --- /dev/null +++ b/lib/services/location_fetcher_service/Fetcher.dart @@ -0,0 +1,104 @@ +import 'package:flutter/foundation.dart'; +import 'package:locus/services/location_fetcher_service/Locations.dart'; +import 'package:locus/services/location_point_service.dart'; +import 'package:locus/services/view_service.dart'; + +class Fetcher extends ChangeNotifier { + final TaskView view; + final Locations _locations = Locations(); + + final List _getLocationsUnsubscribers = []; + + bool _isMounted = true; + bool _isLoading = false; + bool _hasFetchedPreviewLocations = false; + + List get locations => _locations.locations; + + bool get isLoading => _isLoading; + + bool get hasFetchedPreviewLocations => _hasFetchedPreviewLocations; + + Fetcher(this.view); + + void _getLocations({ + final DateTime? from, + final int? limit, + final VoidCallback? onEmptyEnd, + final VoidCallback? onEnd, + final void Function(LocationPointService)? onLocationFetched, + }) { + _isLoading = true; + + notifyListeners(); + + final unsubscriber = view.getLocations( + limit: limit, + from: from, + onLocationFetched: (location) { + if (!_isMounted) { + return; + } + + _locations.add(location); + onLocationFetched?.call(location); + notifyListeners(); + }, + onEnd: () { + if (!_isMounted) { + return; + } + + _isLoading = false; + onEnd?.call(); + notifyListeners(); + }, + onEmptyEnd: () { + if (!_isMounted) { + return; + } + + _isLoading = false; + onEmptyEnd?.call(); + notifyListeners(); + }, + ); + + _getLocationsUnsubscribers.add(unsubscriber); + } + + void fetchPreviewLocations() { + _getLocations( + from: DateTime.now().subtract(const Duration(hours: 24)), + onEnd: () { + _hasFetchedPreviewLocations = true; + }, + onEmptyEnd: () { + _getLocations( + limit: 1, + onEnd: () { + _hasFetchedPreviewLocations = true; + }, + onEmptyEnd: () { + _hasFetchedPreviewLocations = true; + }, + ); + }, + ); + } + + void fetchAllLocations() { + _getLocations(); + } + + @override + void dispose() { + _isMounted = false; + + for (final unsubscriber in _getLocationsUnsubscribers) { + unsubscriber(); + } + + super.dispose(); + } +} diff --git a/lib/services/location_fetcher_service/Locations.dart b/lib/services/location_fetcher_service/Locations.dart new file mode 100644 index 00000000..2686452f --- /dev/null +++ b/lib/services/location_fetcher_service/Locations.dart @@ -0,0 +1,16 @@ +import 'dart:collection'; + +import 'package:locus/services/location_point_service.dart'; + +class Locations { + final Set _locations = {}; + + List get locations => _locations.toList(growable: false) + ..sort((a, b) => a.createdAt.compareTo(b.createdAt)); + + Locations(); + + void add(final LocationPointService location) { + _locations.add(location); + } +} From 5c20b47c9886cc98cbfbb360b8f70b4951f79b2b Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 9 Sep 2023 09:53:34 +0200 Subject: [PATCH 02/15] fix: Fix empty location fetcher --- .../LocationFetchers.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart b/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart index 392e0c1b..b2548d16 100644 --- a/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart +++ b/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart @@ -1,5 +1,6 @@ import 'dart:collection'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:locus/services/location_fetcher_service/Fetcher.dart'; import 'package:locus/services/location_point_service.dart'; @@ -40,11 +41,11 @@ class LocationFetchers extends ChangeNotifier { } } - Fetcher _findFetcher(final TaskView view) { - return _fetchers.firstWhere((fetcher) => fetcher.view == view); + Fetcher? _findFetcher(final TaskView view) { + return _fetchers.firstWhereOrNull((fetcher) => fetcher.view == view); } List getLocations(final TaskView view) { - return _findFetcher(view).locations; + return _findFetcher(view)?.locations ?? []; } } From 120482b951e678a831feeab91910d92dc9eb7a11 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 9 Sep 2023 10:31:30 +0200 Subject: [PATCH 03/15] feat: Show last location circle when all are shown and show all when only one share is shown --- lib/screens/LocationsOverviewScreen.dart | 52 +++++++++++++++--------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/lib/screens/LocationsOverviewScreen.dart b/lib/screens/LocationsOverviewScreen.dart index d6a3e9e7..f94d2cc3 100644 --- a/lib/screens/LocationsOverviewScreen.dart +++ b/lib/screens/LocationsOverviewScreen.dart @@ -836,25 +836,39 @@ class _LocationsOverviewScreenState extends State ), children: [ CircleLayer( - circles: viewService.views.reversed - .where( - (view) => selectedViewID == null || view.id == selectedViewID) - .map( - (view) => mergeLocationsIfRequired(view) - .mapIndexed( - (index, location) => CircleMarker( - radius: location.accuracy, - useRadiusInMeter: true, - point: LatLng(location.latitude, location.longitude), - borderStrokeWidth: 1, - color: view.color.withOpacity(.1), - borderColor: view.color, - ), - ) - .toList(), - ) - .expand((element) => element) - .toList(), + circles: selectedViewID == null + ? _fetchers.fetchers + .where((fetcher) => fetcher.locations.isNotEmpty) + .map((fetcher) { + final location = fetcher.locations.last; + + return CircleMarker( + radius: location.accuracy, + useRadiusInMeter: true, + point: LatLng(location.latitude, location.longitude), + borderStrokeWidth: 1, + color: fetcher.view.color.withOpacity(.1), + borderColor: fetcher.view.color, + ); + }).toList() + : viewService.views + .map( + (view) => mergeLocationsIfRequired(view) + .mapIndexed( + (index, location) => CircleMarker( + radius: location.accuracy, + useRadiusInMeter: true, + point: + LatLng(location.latitude, location.longitude), + borderStrokeWidth: 1, + color: view.color.withOpacity(.1), + borderColor: view.color, + ), + ) + .toList(), + ) + .expand((element) => element) + .toList(), ), PolylineLayer( polylines: List.from( From 1dff5e0ac4357f9806e33e410103469311e3945b Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 9 Sep 2023 10:53:19 +0200 Subject: [PATCH 04/15] chore: Update constraints --- lib/utils/permissions/mixins.dart | 2 +- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/utils/permissions/mixins.dart b/lib/utils/permissions/mixins.dart index 2c5a1de5..0f3890c4 100644 --- a/lib/utils/permissions/mixins.dart +++ b/lib/utils/permissions/mixins.dart @@ -4,7 +4,7 @@ import 'package:nearby_connections/nearby_connections.dart'; import 'has-granted.dart'; -abstract class BluetoothPermissionMixin { +mixin BluetoothPermissionMixin { bool hasGrantedBluetoothPermission = false; setState(VoidCallback fn); diff --git a/pubspec.lock b/pubspec.lock index ee87fe83..61de5a13 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1478,10 +1478,10 @@ packages: dependency: transitive description: name: tuple - sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa" + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index cf7d980b..5bf05317 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 0.14.3+35 environment: - sdk: '>=2.18.5 <3.0.0' + sdk: '>=3.0.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions From 9dcaced752152fe09268d219fdd074d62edadbad Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 9 Sep 2023 11:26:25 +0200 Subject: [PATCH 05/15] feat: Jump to correct position on location change --- lib/screens/LocationsOverviewScreen.dart | 125 +++++++++++------- .../ViewDetails.dart | 9 +- 2 files changed, 78 insertions(+), 56 deletions(-) diff --git a/lib/screens/LocationsOverviewScreen.dart b/lib/screens/LocationsOverviewScreen.dart index f94d2cc3..4faf019f 100644 --- a/lib/screens/LocationsOverviewScreen.dart +++ b/lib/screens/LocationsOverviewScreen.dart @@ -1,5 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:core'; +import 'dart:core'; import 'dart:io'; import 'dart:math'; @@ -105,6 +107,8 @@ class _LocationsOverviewScreenState extends State // Dummy stream to trigger updates to out of bound markers StreamController mapEventStream = StreamController.broadcast(); + LocationPointService? visibleLocation; + // Since we already listen to the latest position, we will pass it // manually to `current_location_layer` to avoid it also registering // extra listeners. @@ -267,9 +271,9 @@ class _LocationsOverviewScreenState extends State ); } - List mergeLocationsIfRequired(final TaskView view) { - final locations = _fetchers.getLocations(view); - + List mergeLocationsIfRequired( + final List locations, + ) { if (locations.isEmpty) { return locations; } @@ -278,20 +282,12 @@ class _LocationsOverviewScreenState extends State return locations; } - if (_cachedMergedLocations.containsKey(selectedView)) { - return _cachedMergedLocations[selectedView]!; - } - final mergedLocations = mergeLocations( locations, distanceThreshold: LOCATION_MERGE_DISTANCE_THRESHOLD, ); return mergedLocations; - - _cachedMergedLocations[view] = mergedLocations; - - return mergedLocations; } void _rebuild() { @@ -741,6 +737,24 @@ class _LocationsOverviewScreenState extends State Widget buildMap() { final settings = context.read(); final viewService = context.read(); + final shades = getPrimaryColorShades(context); + + final Iterable<(TaskView, LocationPointService)> circleLocations = + selectedViewID == null + ? _fetchers.fetchers + .where((fetcher) => fetcher.locations.isNotEmpty) + .map((fetcher) => (fetcher.view, fetcher.locations.last)) + : viewService.views + .map( + (view) => mergeLocationsIfRequired( + _fetchers + .getLocations(view) + .whereNot((location) => location == visibleLocation) + .toList(), + ), + ) + .expand((element) => element) + .map((location) => (selectedView!, location)); if (settings.getMapProvider() == MapProvider.apple) { return apple_maps.AppleMap( @@ -770,7 +784,7 @@ class _LocationsOverviewScreenState extends State .where( (view) => selectedViewID == null || view.id == selectedViewID) .map( - (view) => mergeLocationsIfRequired(view) + (view) => mergeLocationsIfRequired(_fetchers.getLocations(view)) .map( (location) => apple_maps.Circle( circleId: apple_maps.CircleId(location.id), @@ -809,7 +823,7 @@ class _LocationsOverviewScreenState extends State }); }, // TODO - points: mergeLocationsIfRequired(view) + points: mergeLocationsIfRequired(_fetchers.getLocations(view)) .reversed .map( (location) => apple_maps.LatLng( @@ -836,40 +850,39 @@ class _LocationsOverviewScreenState extends State ), children: [ CircleLayer( - circles: selectedViewID == null - ? _fetchers.fetchers - .where((fetcher) => fetcher.locations.isNotEmpty) - .map((fetcher) { - final location = fetcher.locations.last; - - return CircleMarker( - radius: location.accuracy, - useRadiusInMeter: true, - point: LatLng(location.latitude, location.longitude), - borderStrokeWidth: 1, - color: fetcher.view.color.withOpacity(.1), - borderColor: fetcher.view.color, - ); - }).toList() - : viewService.views - .map( - (view) => mergeLocationsIfRequired(view) - .mapIndexed( - (index, location) => CircleMarker( - radius: location.accuracy, - useRadiusInMeter: true, - point: - LatLng(location.latitude, location.longitude), - borderStrokeWidth: 1, - color: view.color.withOpacity(.1), - borderColor: view.color, - ), - ) - .toList(), - ) - .expand((element) => element) - .toList(), + circles: circleLocations + .map((data) { + final view = data.$1; + final location = data.$2; + + return CircleMarker( + radius: location.accuracy, + useRadiusInMeter: true, + point: LatLng(location.latitude, location.longitude), + borderStrokeWidth: 1, + color: view.color.withOpacity(.1), + borderColor: view.color, + ); + }) + .toList() + .cast(), ), + if (visibleLocation != null) + CircleLayer( + circles: [ + CircleMarker( + radius: visibleLocation!.accuracy, + useRadiusInMeter: true, + point: LatLng( + visibleLocation!.latitude, + visibleLocation!.longitude, + ), + borderStrokeWidth: 3, + color: shades[500]!.withOpacity(.3), + borderColor: shades[500]!, + ) + ], + ), PolylineLayer( polylines: List.from( _fetchers.fetchers @@ -878,7 +891,9 @@ class _LocationsOverviewScreenState extends State .map( (fetcher) { final view = fetcher.view; - final locations = mergeLocationsIfRequired(view); + final locations = mergeLocationsIfRequired( + _fetchers.getLocations(view), + ); return Polyline( color: view.color.withOpacity(0.9), @@ -1093,6 +1108,7 @@ class _LocationsOverviewScreenState extends State Navigator.pop(context); setState(() { selectedViewID = null; + visibleLocation = null; }); }, ) @@ -1151,6 +1167,7 @@ class _LocationsOverviewScreenState extends State setState(() { showFAB = true; selectedViewID = null; + visibleLocation = null; }); return; } @@ -1199,6 +1216,7 @@ class _LocationsOverviewScreenState extends State Navigator.pop(context); setState(() { selectedViewID = null; + visibleLocation = null; }); }, ) @@ -1464,8 +1482,11 @@ class _LocationsOverviewScreenState extends State : _fetchers.getLocations(selectedView!), onGoToPosition: (position) { if (flutterMapController != null) { - flutterMapController! - .move(position, flutterMapController!.zoom); + // Get zoom based of accuracy + final radius = position.accuracy / 200; + final zoom = (16 - log(radius) / log(2)).toDouble(); + + flutterMapController!.move(position.asLatLng(), zoom); } if (appleMapController != null) { @@ -1479,6 +1500,11 @@ class _LocationsOverviewScreenState extends State ); } }, + onVisibleLocationChange: (location) { + setState(() { + visibleLocation = location; + }); + }, ), ActiveSharesSheet( visible: selectedViewID == null, @@ -1497,6 +1523,7 @@ class _LocationsOverviewScreenState extends State setState(() { showFAB = true; selectedViewID = null; + visibleLocation = null; }); createNewQuickLocationShare(); diff --git a/lib/screens/locations_overview_screen_widgets/ViewDetails.dart b/lib/screens/locations_overview_screen_widgets/ViewDetails.dart index 7f23c8ed..04a290bd 100644 --- a/lib/screens/locations_overview_screen_widgets/ViewDetails.dart +++ b/lib/screens/locations_overview_screen_widgets/ViewDetails.dart @@ -26,7 +26,7 @@ import '../ViewDetailScreen.dart'; class ViewDetails extends StatefulWidget { final TaskView? view; final LocationPointService? location; - final void Function(LatLng position) onGoToPosition; + final void Function(LocationPointService position) onGoToPosition; const ViewDetails({ required this.view, @@ -116,12 +116,7 @@ class _ViewDetailsState extends State { DistanceBentoElement( lastLocation: lastLocation, onTap: () { - widget.onGoToPosition( - LatLng( - lastLocation.latitude, - lastLocation.longitude, - ), - ); + widget.onGoToPosition(lastLocation); }, ), BentoGridElement( From e272f9bedd874261610d36aca111e74a4031f7c5 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 9 Sep 2023 11:30:22 +0200 Subject: [PATCH 06/15] feat: Jump to correct position on location change --- .../ViewDetailsSheet.dart | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/screens/locations_overview_screen_widgets/ViewDetailsSheet.dart b/lib/screens/locations_overview_screen_widgets/ViewDetailsSheet.dart index ca06f0e9..7acb4e6f 100644 --- a/lib/screens/locations_overview_screen_widgets/ViewDetailsSheet.dart +++ b/lib/screens/locations_overview_screen_widgets/ViewDetailsSheet.dart @@ -13,12 +13,14 @@ import '../../widgets/SimpleAddressFetcher.dart'; class ViewDetailsSheet extends StatefulWidget { final TaskView? view; final List? locations; - final void Function(LatLng position) onGoToPosition; + final void Function(LocationPointService position) onGoToPosition; + final void Function(LocationPointService location) onVisibleLocationChange; const ViewDetailsSheet({ required this.view, required this.locations, required this.onGoToPosition, + required this.onVisibleLocationChange, super.key, }); @@ -139,10 +141,16 @@ class _ViewDetailsSheetState extends State { SizedBox( height: 120, child: PageView.builder( - physics: isExpanded - ? null - : const NeverScrollableScrollPhysics(), + physics: null, onPageChanged: (index) { + final location = widget.locations![index]; + + widget.onVisibleLocationChange(location); + + if (!isExpanded) { + widget.onGoToPosition(location); + } + setState(() { locationIndex = index; }); From 2aecce933fcf1e637b7e0b486c92e19def1f9530 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 9 Sep 2023 11:39:44 +0200 Subject: [PATCH 07/15] feat: Add absolute date value to ViewDetails.dart --- .../ViewDetails.dart | 21 +++++++++-------- lib/utils/date.dart | 23 +++++++++++++++++++ 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/lib/screens/locations_overview_screen_widgets/ViewDetails.dart b/lib/screens/locations_overview_screen_widgets/ViewDetails.dart index 04a290bd..9d0bfb36 100644 --- a/lib/screens/locations_overview_screen_widgets/ViewDetails.dart +++ b/lib/screens/locations_overview_screen_widgets/ViewDetails.dart @@ -8,6 +8,7 @@ import 'package:geolocator/geolocator.dart'; import 'package:get_time_ago/get_time_ago.dart'; import 'package:locus/services/view_service.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:locus/utils/date.dart'; import 'package:locus/utils/navigation.dart'; import 'package:latlong2/latlong.dart'; @@ -304,6 +305,7 @@ class LastLocationBentoElement extends StatefulWidget { class _LastLocationBentoElementState extends State { late final Timer _timer; + bool showAbsolute = false; @override void initState() { @@ -327,16 +329,17 @@ class _LastLocationBentoElementState extends State { return BentoGridElement( onTap: () { - pushRoute( - context, - (context) => ViewDetailScreen(view: widget.view), - ); + setState(() { + showAbsolute = !showAbsolute; + }); }, - title: GetTimeAgo.parse( - DateTime.now().subtract( - DateTime.now().difference(widget.lastLocation.createdAt), - ), - ), + title: showAbsolute + ? formatDateTimeHumanReadable(widget.lastLocation.createdAt) + : GetTimeAgo.parse( + DateTime.now().subtract( + DateTime.now().difference(widget.lastLocation.createdAt), + ), + ), icon: Icons.location_on_rounded, description: l10n.locations_values_lastLocation_description, ); diff --git a/lib/utils/date.dart b/lib/utils/date.dart index 214c64b1..35276c18 100644 --- a/lib/utils/date.dart +++ b/lib/utils/date.dart @@ -1,4 +1,6 @@ // Creates a DateTime that represents the given weekday in the year 2004. Primarily only used for formatting weekdays. +import 'package:intl/intl.dart'; + DateTime createDateFromWeekday(final int day) => DateTime( 2004, 0, @@ -13,3 +15,24 @@ extension DateTimeExtension on DateTime { bool isSameDay(final DateTime other) => year == other.year && month == other.month && day == other.day; } + +String formatDateTimeHumanReadable( + final DateTime dateTime, [ + final DateTime? comparison, +]) { + final compareValue = comparison ?? DateTime.now(); + + if (dateTime.year != compareValue.year) { + return DateFormat.yMMMMd().add_Hms().format(dateTime); + } + + if (dateTime.month != compareValue.month) { + return DateFormat.MMMMd().add_Hms().format(dateTime); + } + + if (dateTime.day != compareValue.day) { + return DateFormat.MMMd().add_Hms().format(dateTime); + } + + return DateFormat.Hms().format(dateTime); +} From 9d035bd7c560e16f2c9c6e5e4be38bc36680dba3 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 9 Sep 2023 12:12:06 +0200 Subject: [PATCH 08/15] feat: Remove TaskDetailScreen.dart's map --- lib/screens/TaskDetailScreen.dart | 212 +----------------- .../TaskTile.dart | 22 +- 2 files changed, 22 insertions(+), 212 deletions(-) diff --git a/lib/screens/TaskDetailScreen.dart b/lib/screens/TaskDetailScreen.dart index a4b1a591..8819103e 100644 --- a/lib/screens/TaskDetailScreen.dart +++ b/lib/screens/TaskDetailScreen.dart @@ -39,52 +39,10 @@ class TaskDetailScreen extends StatefulWidget { } class _TaskDetailScreenState extends State { - final PageController _pageController = PageController(); - late final LocationFetcher _locationFetcher; - bool _isError = false; - bool _isShowingDetails = false; - @override void initState() { super.initState(); - emptyLocationsCount++; - - _locationFetcher = widget.task.createLocationFetcher( - onLocationFetched: (final location) { - emptyLocationsCount = 0; - // Only update partially to avoid lag - EasyThrottle.throttle( - "${widget.task.id}:location-fetch", - DEBOUNCE_DURATION, - () { - if (!mounted) { - return; - } - setState(() {}); - }, - ); - }, - ); - - _locationFetcher.fetchMore( - onEnd: () { - setState(() {}); - }, - ); - - _pageController.addListener(() { - if (_pageController.page == 0) { - setState(() { - _isShowingDetails = false; - }); - } else { - setState(() { - _isShowingDetails = true; - }); - } - }); - WidgetsBinding.instance.addPostFrameCallback((_) async { final settings = context.read(); @@ -144,23 +102,13 @@ class _TaskDetailScreenState extends State { ); } - @override - void dispose() { - _pageController.dispose(); - _locationFetcher.dispose(); - - super.dispose(); - } - @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); return PlatformScaffold( appBar: PlatformAppBar( - title: Text( - _isShowingDetails ? l10n.taskDetails_title : widget.task.name, - ), + title: Text(l10n.taskDetails_title), material: (_, __) => MaterialAppBarData( centerTitle: true, ), @@ -168,19 +116,6 @@ class _TaskDetailScreenState extends State { backgroundColor: getCupertinoAppBarColorForMapScreen(context), ), trailingActions: [ - if (_locationFetcher.controller.locations.isNotEmpty && - !_isShowingDetails) - PlatformIconButton( - cupertino: (_, __) => CupertinoIconButtonData( - padding: EdgeInsets.zero, - ), - icon: const Icon(Icons.my_location_rounded), - onPressed: () { - // No need to check for location permission, as the user must enable it to create locations - // in the first place - _locationFetcher.controller.goToUserLocation(); - }, - ), PlatformIconButton( cupertino: (_, __) => CupertinoIconButtonData( padding: EdgeInsets.zero, @@ -188,146 +123,15 @@ class _TaskDetailScreenState extends State { icon: Icon(context.platformIcons.help), onPressed: showHelp, ), - Padding( - padding: isMaterial(context) - ? const EdgeInsets.all(SMALL_SPACE) - : EdgeInsets.zero, - child: PlatformPopup( - type: PlatformPopupType.tap, - cupertinoButtonPadding: EdgeInsets.zero, - items: [ - PlatformPopupMenuItem( - label: PlatformListTile( - leading: Icon(context.platformIcons.location), - trailing: const SizedBox.shrink(), - title: Text(l10n.viewDetails_actions_openLatestLocation), - ), - onPressed: () async { - await showPlatformModalSheet( - context: context, - material: MaterialModalSheetData( - backgroundColor: Colors.transparent, - ), - builder: (context) => OpenInMaps( - destination: Coords( - _locationFetcher.controller.locations.last.latitude, - _locationFetcher.controller.locations.last.longitude, - ), - ), - ); - }, - ), - // If the fetched locations are less than the limit, - // there are definitely no more locations to fetch - if (_locationFetcher.canFetchMore) - PlatformPopupMenuItem( - label: PlatformListTile( - leading: Icon(context.platformIcons.refresh), - trailing: const SizedBox.shrink(), - title: Text(l10n.locationFetcher_actions_fetchMore), - ), - onPressed: () { - _locationFetcher.fetchMore(onEnd: () { - setState(() {}); - }); - }, - ), - ], - ), - ), ], ), - body: _isError - ? const LocationFetchError() - : PageView( - physics: const NeverScrollableScrollPhysics(), - scrollDirection: Axis.vertical, - controller: _pageController, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - flex: 9, - child: (() { - if (_locationFetcher.controller.locations.isNotEmpty) { - return Stack( - children: [ - LocationsMap( - controller: _locationFetcher.controller, - ), - if (_locationFetcher.isLoading) - const LocationStillFetchingBanner(), - ], - ); - } - - if (_locationFetcher.isLoading) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.all(MEDIUM_SPACE), - child: LocationsLoadingScreen( - locations: - _locationFetcher.controller.locations, - onTimeout: () { - setState(() { - _isError = true; - }); - }, - ), - ), - ); - } - - if (emptyLocationsCount > EMPTY_LOCATION_THRESHOLD) { - return const EmptyLocationsThresholdScreen(); - } - - return const LocationFetchEmpty(); - })(), - ), - Expanded( - flex: 1, - child: PlatformTextButton( - material: (_, __) => MaterialTextButtonData( - style: ButtonStyle( - // Not rounded, but square - shape: MaterialStateProperty.all( - const RoundedRectangleBorder( - borderRadius: BorderRadius.zero, - ), - ), - ), - ), - child: Text(l10n.taskDetails_goToDetails), - onPressed: () { - _pageController.animateToPage( - 1, - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ); - }, - ), - ), - ], - ), - SafeArea( - child: SingleChildScrollView( - child: Details( - locations: _locationFetcher.controller.locations, - task: widget.task, - onGoBack: () { - _pageController.animateToPage( - 0, - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ); - }, - ), - ), - ), - ], - ), + body: SafeArea( + child: SingleChildScrollView( + child: Details( + task: widget.task, + ), + ), + ), ); } } diff --git a/lib/screens/locations_overview_screen_widgets/TaskTile.dart b/lib/screens/locations_overview_screen_widgets/TaskTile.dart index 9ebf0395..6b674cb2 100644 --- a/lib/screens/locations_overview_screen_widgets/TaskTile.dart +++ b/lib/screens/locations_overview_screen_widgets/TaskTile.dart @@ -95,6 +95,20 @@ class _TaskTileState extends State with TaskLinkGenerationMixin { ), onPressed: generateLink, ), + PlatformPopupMenuItem( + label: PlatformListTile( + leading: Icon(context.platformIcons.info), + title: Text(l10n.taskAction_showDetails), + ), + onPressed: () { + pushRoute( + context, + (context) => TaskDetailScreen( + task: widget.task, + ), + ); + }, + ) ], ), leading: FutureBuilder( @@ -117,14 +131,6 @@ class _TaskTileState extends State with TaskLinkGenerationMixin { : null, ), ), - onTap: () { - pushRoute( - context, - (context) => TaskDetailScreen( - task: widget.task, - ), - ); - }, ); } } From 5fea95d136bd5e73921084b23d878fc546eb961b Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 9 Sep 2023 12:12:18 +0200 Subject: [PATCH 09/15] refactor: Make LocationFetchers.dart global --- lib/l10n/app_en.arb | 1 + lib/main.dart | 3 + lib/screens/LocationsOverviewScreen.dart | 98 ++++++++++--------- .../LocationFetchers.dart | 25 ++++- .../task_detail_screen_widgets/Details.dart | 91 ----------------- 5 files changed, 79 insertions(+), 139 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6491826d..c1d703b7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -167,6 +167,7 @@ "taskAction_generateLink_process_publishing": "Publishing encrypted data...", "taskAction_generateLink_process_creatingURI": "Creating link...", "taskAction_generateLink_shareTextSubject": "Here's the link to see my location", + "taskAction_showDetails": "Show Details", "tasks_action_stopAll": "Stop tasks", "tasks_action_startAll": "Start tasks", "tasks_examples_weekend": "Weekend Getaway", diff --git a/lib/main.dart b/lib/main.dart index 49c5c2aa..79c120b5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_logs/flutter_logs.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:locus/App.dart'; +import 'package:locus/screens/locations_overview_screen_widgets/LocationFetchers.dart'; import 'package:locus/services/app_update_service.dart'; import 'package:locus/services/log_service.dart'; import 'package:locus/services/manager_service/background_fetch.dart'; @@ -84,6 +85,8 @@ void main() async { ChangeNotifierProvider(create: (_) => logService), ChangeNotifierProvider( create: (_) => appUpdateService), + ChangeNotifierProvider( + create: (_) => LocationFetchers()), ], child: const App(), ), diff --git a/lib/screens/LocationsOverviewScreen.dart b/lib/screens/LocationsOverviewScreen.dart index 4faf019f..79106391 100644 --- a/lib/screens/LocationsOverviewScreen.dart +++ b/lib/screens/LocationsOverviewScreen.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:core'; -import 'dart:core'; import 'dart:io'; import 'dart:math'; @@ -91,7 +90,6 @@ class _LocationsOverviewScreenState extends State AutomaticKeepAliveClientMixin, WidgetsBindingObserver, TickerProviderStateMixin { - final _fetchers = LocationFetchers()..enableLocationsUpdates(); MapController? flutterMapController; PopupController? flutterMapPopupController; apple_maps.AppleMapController? appleMapController; @@ -146,12 +144,16 @@ class _LocationsOverviewScreenState extends State final logService = context.read(); final settings = context.read(); final appUpdateService = context.read(); + final locationFetchers = context.read(); _handleViewAlarmChecker(); _handleNotifications(); + locationFetchers.addAll(viewService.views); + settings.addListener(_updateBackgroundListeners); taskService.addListener(_updateBackgroundListeners); + locationFetchers.addLocationUpdatesListener(_rebuild); WidgetsBinding.instance ..addObserver(this) @@ -162,7 +164,7 @@ class _LocationsOverviewScreenState extends State _initUniLinks(); _updateLocaleToSettings(); _showUpdateDialogIfRequired(); - _initFetchers(); + locationFetchers.fetchPreviewLocations(); taskService.checkup(logService); @@ -202,8 +204,10 @@ class _LocationsOverviewScreenState extends State @override dispose() { + final appUpdateService = context.read(); + final locationFetchers = context.read(); + flutterMapController?.dispose(); - _fetchers.dispose(); _viewsAlarmCheckerTimer?.cancel(); _uniLinksStream?.cancel(); @@ -213,8 +217,8 @@ class _LocationsOverviewScreenState extends State WidgetsBinding.instance.removeObserver(this); - final appUpdateService = context.read(); appUpdateService.removeListener(_rebuild); + locationFetchers.removeLocationUpdatesListener(_rebuild); super.dispose(); } @@ -232,21 +236,14 @@ class _LocationsOverviewScreenState extends State } } - void _initFetchers() { - final viewService = context.read(); - - _fetchers.addListener(_rebuild); - - _fetchers.addAll(viewService.views); - _fetchers.fetchPreviewLocations(); - } - void _handleViewServiceChange() { final viewService = context.read(); + final locationFetchers = context.read(); + final newView = viewService.views.last; - _fetchers.add(newView); - _fetchers.fetchPreviewLocations(); + locationFetchers.add(newView); + locationFetchers.fetchPreviewLocations(); } void _setLocationFromSettings() async { @@ -737,17 +734,19 @@ class _LocationsOverviewScreenState extends State Widget buildMap() { final settings = context.read(); final viewService = context.read(); + final locationFetchers = context.read(); + final shades = getPrimaryColorShades(context); final Iterable<(TaskView, LocationPointService)> circleLocations = selectedViewID == null - ? _fetchers.fetchers + ? locationFetchers.fetchers .where((fetcher) => fetcher.locations.isNotEmpty) .map((fetcher) => (fetcher.view, fetcher.locations.last)) : viewService.views .map( (view) => mergeLocationsIfRequired( - _fetchers + locationFetchers .getLocations(view) .whereNot((location) => location == visibleLocation) .toList(), @@ -784,25 +783,26 @@ class _LocationsOverviewScreenState extends State .where( (view) => selectedViewID == null || view.id == selectedViewID) .map( - (view) => mergeLocationsIfRequired(_fetchers.getLocations(view)) - .map( - (location) => apple_maps.Circle( - circleId: apple_maps.CircleId(location.id), - center: apple_maps.LatLng( - location.latitude, - location.longitude, - ), - radius: location.accuracy, - fillColor: view.color.withOpacity(0.2), - strokeColor: view.color, - strokeWidth: location.accuracy < 10 ? 1 : 3), - ) - .toList(), + (view) => + mergeLocationsIfRequired(locationFetchers.getLocations(view)) + .map( + (location) => apple_maps.Circle( + circleId: apple_maps.CircleId(location.id), + center: apple_maps.LatLng( + location.latitude, + location.longitude, + ), + radius: location.accuracy, + fillColor: view.color.withOpacity(0.2), + strokeColor: view.color, + strokeWidth: location.accuracy < 10 ? 1 : 3), + ) + .toList(), ) .expand((element) => element) .toSet(), polylines: Set.from( - _fetchers.fetchers + locationFetchers.fetchers .where((fetcher) => selectedViewID == null || fetcher.view.id == selectedViewID) .map( @@ -823,7 +823,8 @@ class _LocationsOverviewScreenState extends State }); }, // TODO - points: mergeLocationsIfRequired(_fetchers.getLocations(view)) + points: mergeLocationsIfRequired( + locationFetchers.getLocations(view)) .reversed .map( (location) => apple_maps.LatLng( @@ -885,14 +886,14 @@ class _LocationsOverviewScreenState extends State ), PolylineLayer( polylines: List.from( - _fetchers.fetchers + locationFetchers.fetchers .where((fetcher) => selectedViewID == null || fetcher.view.id == selectedViewID) .map( (fetcher) { final view = fetcher.view; final locations = mergeLocationsIfRequired( - _fetchers.getLocations(view), + locationFetchers.getLocations(view), ); return Polyline( @@ -943,9 +944,9 @@ class _LocationsOverviewScreenState extends State markers: viewService.views .where((view) => (selectedViewID == null || view.id == selectedViewID) && - _fetchers.getLocations(view).isNotEmpty) + locationFetchers.getLocations(view).isNotEmpty) .map((view) { - final latestLocation = _fetchers.getLocations(view).last; + final latestLocation = locationFetchers.getLocations(view).last; return Marker( key: Key(view.id), @@ -975,8 +976,10 @@ class _LocationsOverviewScreenState extends State } Widget buildOutOfBoundsMarkers() { + final locationFetchers = context.read(); + return Stack( - children: _fetchers.fetchers + children: locationFetchers.fetchers .where((fetcher) => (selectedViewID == null || fetcher.view.id == selectedViewID) && fetcher.locations.isNotEmpty) @@ -996,8 +999,12 @@ class _LocationsOverviewScreenState extends State ); } - void showViewLocations(final TaskView view, - {final bool jumpToLatestLocation = true}) async { + void showViewLocations( + final TaskView view, { + final bool jumpToLatestLocation = true, + }) async { + final locationFetchers = context.read(); + setState(() { showFAB = false; selectedViewID = view.id; @@ -1007,7 +1014,7 @@ class _LocationsOverviewScreenState extends State return; } - final locations = _fetchers.getLocations(view); + final locations = locationFetchers.getLocations(view); if (locations.isEmpty) { return; @@ -1250,11 +1257,13 @@ class _LocationsOverviewScreenState extends State } LocationPointService? get lastLocation { + final locationFetchers = context.read(); + if (selectedView == null) { return null; } - final locations = _fetchers.getLocations(selectedView!); + final locations = locationFetchers.getLocations(selectedView!); if (locations.isEmpty) { return null; } @@ -1393,6 +1402,7 @@ class _LocationsOverviewScreenState extends State super.build(context); final settings = context.watch(); + final locationFetchers = context.watch(); final l10n = AppLocalizations.of(context); return PlatformScaffold( @@ -1479,7 +1489,7 @@ class _LocationsOverviewScreenState extends State view: selectedView, locations: selectedViewID == null ? [] - : _fetchers.getLocations(selectedView!), + : locationFetchers.getLocations(selectedView!), onGoToPosition: (position) { if (flutterMapController != null) { // Get zoom based of accuracy diff --git a/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart b/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart index b2548d16..46236956 100644 --- a/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart +++ b/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart @@ -1,5 +1,3 @@ -import 'dart:collection'; - import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:locus/services/location_fetcher_service/Fetcher.dart'; @@ -13,9 +11,19 @@ class LocationFetchers extends ChangeNotifier { LocationFetchers(); - void enableLocationsUpdates() { + void addLocationUpdatesListener( + final VoidCallback callback, + ) { + for (final fetcher in _fetchers) { + fetcher.addListener(callback); + } + } + + void removeLocationUpdatesListener( + final VoidCallback callback, + ) { for (final fetcher in _fetchers) { - fetcher.addListener(notifyListeners); + fetcher.removeListener(callback); } } @@ -27,6 +35,15 @@ class LocationFetchers extends ChangeNotifier { _fetchers.add(Fetcher(view)); } + void remove(final TaskView view) { + final fetcher = _findFetcher(view); + + if (fetcher != null) { + fetcher.dispose(); + _fetchers.remove(fetcher); + } + } + void addAll(final List views) { for (final view in views) { add(view); diff --git a/lib/screens/task_detail_screen_widgets/Details.dart b/lib/screens/task_detail_screen_widgets/Details.dart index 0a5ace2f..97983ee1 100644 --- a/lib/screens/task_detail_screen_widgets/Details.dart +++ b/lib/screens/task_detail_screen_widgets/Details.dart @@ -20,14 +20,10 @@ import '../../widgets/PlatformListTile.dart'; import '../../widgets/TimerWidgetSheet.dart'; class Details extends StatefulWidget { - final List locations; final Task task; - final void Function() onGoBack; const Details({ - required this.locations, required this.task, - required this.onGoBack, Key? key, }) : super(key: key); @@ -70,24 +66,6 @@ class _DetailsState extends State
{ return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - PlatformTextButton( - material: (_, __) => MaterialTextButtonData( - style: ButtonStyle( - // Not rounded, but square - shape: MaterialStateProperty.all( - const RoundedRectangleBorder( - borderRadius: BorderRadius.zero, - ), - ), - padding: MaterialStateProperty.all( - const EdgeInsets.all(MEDIUM_SPACE), - ), - ), - icon: const Icon(Icons.arrow_upward_rounded), - ), - onPressed: widget.onGoBack, - child: Text(l10n.goBack), - ), Padding( padding: const EdgeInsets.all(MEDIUM_SPACE), child: Wrap( @@ -99,75 +77,6 @@ class _DetailsState extends State
{ task: widget.task, ), ), - DetailInformationBox( - title: l10n.taskDetails_lastKnownLocation, - child: widget.locations.isEmpty - ? Text(l10n.taskDetails_noLocations) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SimpleAddressFetcher( - location: widget.locations.last.asLatLng(), - ), - const SizedBox(height: MEDIUM_SPACE), - Tooltip( - message: - l10n.taskDetails_mostRecentLocationExplanation, - textAlign: TextAlign.center, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon( - context.platformIcons.time, - size: getIconSizeForBodyText(context), - ), - const SizedBox(width: TINY_SPACE), - Text( - widget.locations.last.createdAt.toString(), - style: getBodyTextTextStyle(context), - textAlign: TextAlign.start, - ), - ], - ), - ), - ], - ), - ), - GestureDetector( - onTap: widget.locations.isEmpty - ? null - : () { - Navigator.of(context).push( - PageRouteBuilder( - opaque: true, - fullscreenDialog: true, - barrierColor: Colors.black.withOpacity(0.7), - barrierDismissible: true, - pageBuilder: (context, _, __) => - LocationPointsDetailsScreen( - locations: widget.locations, - isPreview: false, - ), - ), - ); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.taskDetails_locationDetails, - textAlign: TextAlign.start, - style: getSubTitleTextStyle(context), - ), - const SizedBox(height: MEDIUM_SPACE), - LocationPointsDetailsScreen( - locations: widget.locations, - isPreview: true, - ), - ], - ), - ), DetailInformationBox( title: l10n.nostrRelaysLabel, child: Column( From c34a2f516fcb779860b97466179fb3d74cac4e0c Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 9 Sep 2023 12:22:20 +0200 Subject: [PATCH 10/15] feat: Improve distance bento element --- lib/l10n/app_en.arb | 9 +++ .../ViewDetails.dart | 56 ++++++++++--------- lib/services/location_point_service.dart | 3 + 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c1d703b7..1e9d191a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -767,6 +767,15 @@ "locations_values_lastLocation_description": "Last location update", "locations_values_distance_description": "Distance", "locations_values_distance_permissionRequired": "Grant permission", + "locations_values_distance_nearby": "<10 m", + "locations_values_distance_m": "{distance} m", + "@locations_values_distance_m": { + "placeholders": { + "distance": { + "type": "String" + } + } + }, "locations_values_distance_km": "{distance, select, 0 {<1 km} 1 {one km} other {{distance} km}}", "@locations_values_distance_km": { "placeholders": { diff --git a/lib/screens/locations_overview_screen_widgets/ViewDetails.dart b/lib/screens/locations_overview_screen_widgets/ViewDetails.dart index 9d0bfb36..7b04e79c 100644 --- a/lib/screens/locations_overview_screen_widgets/ViewDetails.dart +++ b/lib/screens/locations_overview_screen_widgets/ViewDetails.dart @@ -15,6 +15,8 @@ import 'package:latlong2/latlong.dart'; import 'package:locus/utils/location/index.dart'; import 'package:locus/utils/permissions/has-granted.dart'; import 'package:locus/utils/permissions/request.dart'; +import 'package:locus/widgets/OpenInMaps.dart'; +import 'package:map_launcher/map_launcher.dart'; import '../../constants/spacing.dart'; import '../../services/location_point_service.dart'; import '../../utils/icon.dart'; @@ -116,9 +118,6 @@ class _ViewDetailsState extends State { ), DistanceBentoElement( lastLocation: lastLocation, - onTap: () { - widget.onGoToPosition(lastLocation); - }, ), BentoGridElement( title: lastLocation.altitude == null @@ -184,10 +183,8 @@ class _ViewDetailsState extends State { class DistanceBentoElement extends StatefulWidget { final LocationPointService lastLocation; - final VoidCallback onTap; const DistanceBentoElement({ - required this.onTap, required this.lastLocation, super.key, }); @@ -238,19 +235,15 @@ class _DistanceBentoElementState extends State final l10n = AppLocalizations.of(context); return BentoGridElement( - onTap: hasGrantedPermission == false - ? () async { - final hasGranted = await requestBasicLocationPermission(); - - if (hasGranted) { - fetchCurrentPosition(); - - setState(() { - hasGrantedPermission = true; - }); - } - } - : widget.onTap, + onTap: () { + showPlatformModalSheet( + context: context, + material: MaterialModalSheetData(), + builder: (context) => OpenInMaps( + destination: widget.lastLocation.asCoords(), + ), + ); + }, title: (() { if (!hasGrantedPermission) { return l10n.locations_values_distance_permissionRequired; @@ -260,16 +253,25 @@ class _DistanceBentoElementState extends State return l10n.loading; } + final distanceInMeters = Geolocator.distanceBetween( + currentPosition!.latitude, + currentPosition!.longitude, + widget.lastLocation.latitude, + widget.lastLocation.longitude, + ); + + if (distanceInMeters < 10) { + return l10n.locations_values_distance_nearby; + } + + if (distanceInMeters < 1000) { + return l10n.locations_values_distance_m( + distanceInMeters.toStringAsFixed(0).toString(), + ); + } + return l10n.locations_values_distance_km( - (Geolocator.distanceBetween( - currentPosition!.latitude, - currentPosition!.longitude, - widget.lastLocation.latitude, - widget.lastLocation.longitude, - ) / - 1000) - .floor() - .toString(), + (distanceInMeters / 1000).toStringAsFixed(0), ); })(), type: hasGrantedPermission && currentPosition != null diff --git a/lib/services/location_point_service.dart b/lib/services/location_point_service.dart index 517a4fd1..cfd75e87 100644 --- a/lib/services/location_point_service.dart +++ b/lib/services/location_point_service.dart @@ -7,6 +7,7 @@ import 'package:cryptography/cryptography.dart'; import 'package:flutter/services.dart'; import 'package:geolocator/geolocator.dart'; import 'package:locus/utils/cryptography/decrypt.dart'; +import 'package:map_launcher/map_launcher.dart'; import 'package:uuid/uuid.dart'; const uuid = Uuid(); @@ -204,6 +205,8 @@ class LocationPointService { LatLng asLatLng() => LatLng(latitude, longitude); + Coords asCoords() => Coords(latitude, longitude); + LocationPointService copyWith({ final double? latitude, final double? longitude, From 797b16e60dc1e962aba202f288cbb5656bd7713a Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 9 Sep 2023 13:24:20 +0200 Subject: [PATCH 11/15] feat: Improve view details --- lib/screens/LocationsOverviewScreen.dart | 4 +- lib/screens/ViewDetailScreen.dart | 482 ------------------ lib/screens/ViewDetailsScreen.dart | 168 ++++++ .../LocationFetchers.dart | 6 +- .../ViewDetails.dart | 2 +- .../ViewTile.dart | 4 +- .../LocationPointsList.dart | 86 ++++ .../location_fetcher_service/Fetcher.dart | 31 ++ lib/services/view_service.dart | 2 + 9 files changed, 295 insertions(+), 490 deletions(-) delete mode 100644 lib/screens/ViewDetailScreen.dart create mode 100644 lib/screens/ViewDetailsScreen.dart create mode 100644 lib/screens/view_details_screen_widgets/LocationPointsList.dart diff --git a/lib/screens/LocationsOverviewScreen.dart b/lib/screens/LocationsOverviewScreen.dart index 79106391..a3b82037 100644 --- a/lib/screens/LocationsOverviewScreen.dart +++ b/lib/screens/LocationsOverviewScreen.dart @@ -64,7 +64,7 @@ import '../utils/PageRoute.dart'; import '../utils/color.dart'; import '../utils/platform.dart'; import '../utils/theme.dart'; -import 'ViewDetailScreen.dart'; +import 'ViewDetailsScreen.dart'; import 'locations_overview_screen_widgets/ViewDetailsSheet.dart'; import 'locations_overview_screen_widgets/constants.dart'; @@ -513,7 +513,7 @@ class _LocationsOverviewScreenState extends State Navigator.of(context).push( NativePageRoute( context: context, - builder: (_) => ViewDetailScreen( + builder: (_) => ViewDetailsScreen( view: viewService.getViewById(data["taskViewID"]), ), ), diff --git a/lib/screens/ViewDetailScreen.dart b/lib/screens/ViewDetailScreen.dart deleted file mode 100644 index 2aeabde4..00000000 --- a/lib/screens/ViewDetailScreen.dart +++ /dev/null @@ -1,482 +0,0 @@ -import 'dart:async'; - -import 'package:easy_debounce/easy_throttle.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart' - hide PlatformListTile; -import 'package:geolocator/geolocator.dart'; -import 'package:locus/screens/view_alarm_screen_widgets/ViewAlarmScreen.dart'; -import 'package:locus/screens/view_details_screen_widgets/ViewLocationPointsScreen.dart'; -import 'package:locus/services/location_alarm_service.dart'; -import 'package:locus/services/view_service.dart'; -import 'package:locus/utils/PageRoute.dart'; -import 'package:locus/utils/bunny.dart'; -import 'package:locus/utils/permissions/has-granted.dart'; -import 'package:locus/utils/permissions/request.dart'; -import 'package:locus/widgets/EmptyLocationsThresholdScreen.dart'; -import 'package:locus/widgets/FillUpPaint.dart'; -import 'package:locus/widgets/LocationFetchEmpty.dart'; -import 'package:locus/widgets/LocationsMap.dart'; -import 'package:locus/widgets/OpenInMaps.dart'; -import 'package:locus/widgets/PlatformFlavorWidget.dart'; -import 'package:locus/widgets/PlatformPopup.dart'; -import 'package:map_launcher/map_launcher.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; - -import '../constants/spacing.dart'; -import '../services/location_fetch_controller.dart'; -import '../services/location_point_service.dart'; -import '../utils/theme.dart'; -import '../widgets/LocationFetchError.dart'; -import '../widgets/LocationStillFetchingBanner.dart'; -import '../widgets/LocationsLoadingScreen.dart'; -import '../widgets/PlatformListTile.dart'; - -const DEBOUNCE_DURATION = Duration(seconds: 2); - -class LineSliderTickMarkShape extends SliderTickMarkShape { - final double tickWidth; - - const LineSliderTickMarkShape({ - this.tickWidth = 1.0, - }) : super(); - - @override - Size getPreferredSize( - {required SliderThemeData sliderTheme, required bool isEnabled}) { - // We don't need this - return Size.zero; - } - - @override - void paint( - PaintingContext context, - Offset center, { - required RenderBox parentBox, - required SliderThemeData sliderTheme, - required Animation enableAnimation, - required Offset thumbCenter, - required bool isEnabled, - required TextDirection textDirection, - }) { - // This block is just copied from `slider_theme` - final bool isTickMarkRightOfThumb = center.dx > thumbCenter.dx; - final begin = isTickMarkRightOfThumb - ? sliderTheme.disabledInactiveTickMarkColor - : sliderTheme.disabledActiveTickMarkColor; - final end = isTickMarkRightOfThumb - ? sliderTheme.inactiveTickMarkColor - : sliderTheme.activeTickMarkColor; - final Paint paint = Paint() - ..color = ColorTween(begin: begin, end: end).evaluate(enableAnimation)!; - - final trackHeight = sliderTheme.trackHeight!; - - final rect = Rect.fromCenter( - center: center, - width: tickWidth, - height: trackHeight, - ); - - context.canvas.drawRect(rect, paint); - } -} - -class ViewDetailScreen extends StatefulWidget { - final TaskView view; - - const ViewDetailScreen({ - required this.view, - Key? key, - }) : super(key: key); - - @override - State createState() => _ViewDetailScreenState(); -} - -class _ViewDetailScreenState extends State { - // `_controller` is used to control the actively shown locations on the map - final LocationsMapController _controller = LocationsMapController(); - - // `_locationFetcher.controller` is used to control ALL locations - late final LocationFetcher _locationFetcher; - StreamSubscription? _positionUpdateStream; - - bool _isError = false; - - bool showAlarms = true; - - double? distanceToLatestLocation; - - @override - void initState() { - super.initState(); - - emptyLocationsCount++; - - _locationFetcher = widget.view.createLocationFetcher( - onLocationFetched: (final location) { - emptyLocationsCount = 0; - - _controller.add(location); - // Only update partially to avoid lag - EasyThrottle.throttle( - "${widget.view.id}:location-fetch", - DEBOUNCE_DURATION, - () { - if (!mounted) { - return; - } - setState(() {}); - }, - ); - }, - ); - - _locationFetcher.fetchMore( - onEnd: () { - _updateDistanceToLocation(); - setState(() {}); - }, - ); - - // Update UI when for example alarms are added or removed - widget.view.addListener(updateView); - } - - void _updateDistanceToLocation() async { - if (_locationFetcher.controller.locations.isEmpty || - _positionUpdateStream != null || - !(await hasGrantedLocationPermission())) { - return; - } - - _positionUpdateStream = Geolocator.getPositionStream().listen((position) { - if (!mounted) { - return; - } - - setState(() { - distanceToLatestLocation = Geolocator.distanceBetween( - position.latitude, - position.longitude, - _locationFetcher.controller.locations.last.latitude, - _locationFetcher.controller.locations.last.longitude, - ); - }); - }); - } - - void updateView() { - setState(() {}); - } - - @override - void dispose() { - _locationFetcher.dispose(); - _controller.dispose(); - _positionUpdateStream?.cancel(); - widget.view.removeListener(updateView); - - super.dispose(); - } - - VoidCallback handleTapOnDate(final Iterable locations) { - return () { - _controller.clear(); - - if (locations.isNotEmpty) { - _controller.addAll(locations); - _controller.goTo(locations.last); - } - - setState(() {}); - }; - } - - Widget buildDateSelectButton( - final List locations, - final int hour, - final int maxLocations, - ) { - final shades = getPrimaryColorShades(context); - - return FillUpPaint( - color: shades[0]!, - fillPercentage: - maxLocations == 0 ? 0 : (locations.length.toDouble() / maxLocations), - size: Size( - MediaQuery.of(context).size.width / 24, - MediaQuery.of(context).size.height * (1 / 12), - ), - ); - } - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context); - final locationsPerHour = _locationFetcher.controller.getLocationsPerHour(); - final maxLocations = locationsPerHour.values.isEmpty - ? 0 - : locationsPerHour.values.fold( - 0, - (value, element) => - value > element.length ? value : element.length); - - return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(l10n.viewDetails_title), - trailingActions: [ - if (_locationFetcher.controller.locations.isNotEmpty) - PlatformIconButton( - cupertino: (_, __) => CupertinoIconButtonData( - padding: EdgeInsets.zero, - ), - icon: const Icon(Icons.my_location_rounded), - onPressed: () async { - final hasGrantedLocation = - await requestBasicLocationPermission(); - - if (hasGrantedLocation) { - _controller.goToUserLocation(); - } - }, - ), - if (widget.view.alarms.isNotEmpty && _controller.locations.isNotEmpty) - Tooltip( - message: showAlarms - ? l10n.viewDetails_actions_showAlarms_hide - : l10n.viewDetails_actions_showAlarms_show, - child: PlatformTextButton( - cupertino: (_, __) => CupertinoTextButtonData( - padding: EdgeInsets.zero, - ), - onPressed: () { - setState(() { - showAlarms = !showAlarms; - }); - }, - child: PlatformFlavorWidget( - material: (_, __) => showAlarms - ? const Icon(Icons.alarm_rounded) - : const Icon(Icons.alarm_off_rounded), - cupertino: (_, __) => showAlarms - ? const Icon(CupertinoIcons.alarm) - : const Icon(Icons.alarm_off_rounded), - ), - ), - ), - Padding( - padding: isMaterial(context) - ? const EdgeInsets.all(SMALL_SPACE) - : EdgeInsets.zero, - child: PlatformPopup( - cupertinoButtonPadding: EdgeInsets.zero, - type: PlatformPopupType.tap, - items: [ - PlatformPopupMenuItem( - label: PlatformListTile( - leading: PlatformFlavorWidget( - cupertino: (_, __) => const Icon(CupertinoIcons.alarm), - material: (_, __) => const Icon(Icons.alarm_rounded), - ), - title: Text(l10n.location_manageAlarms_title), - trailing: const SizedBox.shrink(), - ), - onPressed: () { - if (isCupertino(context)) { - Navigator.of(context).push( - MaterialWithModalsPageRoute( - builder: (_) => ViewAlarmScreen(view: widget.view), - ), - ); - } else { - Navigator.of(context).push( - NativePageRoute( - context: context, - builder: (_) => ViewAlarmScreen(view: widget.view), - ), - ); - } - }), - if (_locationFetcher.controller.locations.isNotEmpty) - PlatformPopupMenuItem( - label: PlatformListTile( - leading: Icon(context.platformIcons.location), - trailing: const SizedBox.shrink(), - title: Text(l10n.viewDetails_actions_openLatestLocation), - ), - onPressed: () => showPlatformModalSheet( - context: context, - material: MaterialModalSheetData(), - builder: (context) => OpenInMaps( - destination: Coords( - _locationFetcher.controller.locations.last.latitude, - _locationFetcher.controller.locations.last.longitude, - ), - ), - ), - ), - if (_locationFetcher.controller.locations.isNotEmpty) - PlatformPopupMenuItem( - label: PlatformListTile( - leading: PlatformFlavorWidget( - material: (_, __) => const Icon(Icons.list_rounded), - cupertino: (_, __) => - const Icon(CupertinoIcons.list_bullet), - ), - trailing: const SizedBox.shrink(), - title: Text(l10n.viewDetails_actions_showLocationList), - ), - onPressed: () { - Navigator.push( - context, - NativePageRoute( - context: context, - builder: (context) => ViewLocationPointsScreen( - locationFetcher: _locationFetcher, - ), - ), - ); - }, - ), - ], - ), - ), - ], - material: (_, __) => MaterialAppBarData( - centerTitle: true, - ), - cupertino: (_, __) => CupertinoNavigationBarData( - backgroundColor: getCupertinoAppBarColorForMapScreen(context), - ), - ), - body: (() { - if (_isError) { - return const LocationFetchError(); - } - - if (_locationFetcher.controller.locations.isNotEmpty) { - return PageView( - physics: const NeverScrollableScrollPhysics(), - children: [ - Column( - children: [ - Expanded( - flex: 11, - child: Stack( - children: [ - LocationsMap( - controller: _controller, - showCircles: showAlarms, - circles: List.from( - List.from( - widget.view.alarms) - .map( - (final alarm) => LocationsMapCircle( - id: alarm.id, - center: alarm.center, - radius: alarm.radius, - color: Colors.red.withOpacity(.3), - strokeColor: Colors.red, - ), - ), - )), - if (_locationFetcher.isLoading) - const LocationStillFetchingBanner(), - if (distanceToLatestLocation != null) - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - l10n.viewDetails_distanceToLatestLocation_label( - distanceToLatestLocation!.round(), - ), - style: TextStyle( - color: getIsDarkMode(context) - ? Colors.white - : isCupertino(context) - ? CupertinoColors.secondaryLabel - : Colors.black87, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ) - ], - ), - ), - Expanded( - flex: 1, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: - List.generate(24, (index) => 23 - index).map((hour) { - final date = - DateTime.now().subtract(Duration(hours: hour)); - final normalizedDate = - LocationsMapController.normalizeDateTime(date); - final locations = - locationsPerHour[normalizedDate] ?? []; - final child = buildDateSelectButton( - locations, - hour, - maxLocations, - ); - - return PlatformWidget( - material: (_, __) => InkWell( - onTap: handleTapOnDate(locations), - child: child, - ), - cupertino: (_, __) => GestureDetector( - onTap: handleTapOnDate(locations), - child: child, - ), - ); - }).toList(), - ), - ), - ], - ), - ], - ); - } - - if (_locationFetcher.isLoading) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.all(MEDIUM_SPACE), - child: LocationsLoadingScreen( - locations: _locationFetcher.controller.locations, - onTimeout: () { - setState(() { - _isError = true; - }); - }, - ), - ), - ); - } - - if (emptyLocationsCount > EMPTY_LOCATION_THRESHOLD) { - return const EmptyLocationsThresholdScreen(); - } - - return const LocationFetchEmpty(); - })(), - ); - } -} diff --git a/lib/screens/ViewDetailsScreen.dart b/lib/screens/ViewDetailsScreen.dart new file mode 100644 index 00000000..355d89c8 --- /dev/null +++ b/lib/screens/ViewDetailsScreen.dart @@ -0,0 +1,168 @@ +import 'dart:async'; + +import 'package:easy_debounce/easy_throttle.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart' + hide PlatformListTile; +import 'package:geolocator/geolocator.dart'; +import 'package:locus/screens/view_alarm_screen_widgets/ViewAlarmScreen.dart'; +import 'package:locus/screens/view_details_screen_widgets/LocationPointsList.dart'; +import 'package:locus/screens/view_details_screen_widgets/ViewLocationPointsScreen.dart'; +import 'package:locus/services/location_alarm_service.dart'; +import 'package:locus/services/view_service.dart'; +import 'package:locus/utils/PageRoute.dart'; +import 'package:locus/utils/bunny.dart'; +import 'package:locus/utils/permissions/has-granted.dart'; +import 'package:locus/utils/permissions/request.dart'; +import 'package:locus/widgets/EmptyLocationsThresholdScreen.dart'; +import 'package:locus/widgets/FillUpPaint.dart'; +import 'package:locus/widgets/LocationFetchEmpty.dart'; +import 'package:locus/widgets/LocationsMap.dart'; +import 'package:locus/widgets/OpenInMaps.dart'; +import 'package:locus/widgets/Paper.dart'; +import 'package:locus/widgets/PlatformFlavorWidget.dart'; +import 'package:locus/widgets/PlatformPopup.dart'; +import 'package:map_launcher/map_launcher.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:provider/provider.dart'; + +import '../constants/spacing.dart'; +import '../services/location_fetch_controller.dart'; +import '../services/location_point_service.dart'; +import '../utils/theme.dart'; +import '../widgets/LocationFetchError.dart'; +import '../widgets/LocationStillFetchingBanner.dart'; +import '../widgets/LocationsLoadingScreen.dart'; +import '../widgets/PlatformListTile.dart'; +import 'locations_overview_screen_widgets/LocationFetchers.dart'; + +const DEBOUNCE_DURATION = Duration(seconds: 2); + +class ViewDetailsScreen extends StatefulWidget { + final TaskView view; + + const ViewDetailsScreen({ + super.key, + required this.view, + }); + + @override + State createState() => _ViewDetailsScreenState(); +} + +class _ViewDetailsScreenState extends State { + bool showAlarms = true; + + @override + Widget build(BuildContext context) { + final locationFetcher = context.watch(); + + final locations = locationFetcher.getLocations(widget.view); + final l10n = AppLocalizations.of(context); + + return PlatformScaffold( + appBar: PlatformAppBar( + title: Text(l10n.viewDetails_title), + trailingActions: [ + if (widget.view.alarms.isNotEmpty) + Tooltip( + message: showAlarms + ? l10n.viewDetails_actions_showAlarms_hide + : l10n.viewDetails_actions_showAlarms_show, + child: PlatformTextButton( + cupertino: (_, __) => CupertinoTextButtonData( + padding: EdgeInsets.zero, + ), + onPressed: () { + setState(() { + showAlarms = !showAlarms; + }); + }, + child: PlatformFlavorWidget( + material: (_, __) => showAlarms + ? const Icon(Icons.alarm_rounded) + : const Icon(Icons.alarm_off_rounded), + cupertino: (_, __) => showAlarms + ? const Icon(CupertinoIcons.alarm) + : const Icon(Icons.alarm_off_rounded), + ), + ), + ), + Padding( + padding: isMaterial(context) + ? const EdgeInsets.all(SMALL_SPACE) + : EdgeInsets.zero, + child: PlatformPopup( + cupertinoButtonPadding: EdgeInsets.zero, + type: PlatformPopupType.tap, + items: [ + PlatformPopupMenuItem( + label: PlatformListTile( + leading: PlatformFlavorWidget( + cupertino: (_, __) => const Icon(CupertinoIcons.alarm), + material: (_, __) => const Icon(Icons.alarm_rounded), + ), + title: Text(l10n.location_manageAlarms_title), + trailing: const SizedBox.shrink(), + ), + onPressed: () { + if (isCupertino(context)) { + Navigator.of(context).push( + MaterialWithModalsPageRoute( + builder: (_) => ViewAlarmScreen(view: widget.view), + ), + ); + } else { + Navigator.of(context).push( + NativePageRoute( + context: context, + builder: (_) => ViewAlarmScreen(view: widget.view), + ), + ); + } + }), + if (locations.isNotEmpty) + PlatformPopupMenuItem( + label: PlatformListTile( + leading: Icon(context.platformIcons.location), + trailing: const SizedBox.shrink(), + title: Text(l10n.viewDetails_actions_openLatestLocation), + ), + onPressed: () => showPlatformModalSheet( + context: context, + material: MaterialModalSheetData(), + builder: (context) => OpenInMaps( + destination: locations.last.asCoords(), + ), + ), + ), + ], + ), + ), + ], + material: (_, __) => MaterialAppBarData( + centerTitle: true, + ), + cupertino: (_, __) => CupertinoNavigationBarData( + backgroundColor: getCupertinoAppBarColorForMapScreen(context), + ), + ), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(MEDIUM_SPACE), + child: Column( + children: [ + Paper( + child: LocationPointsList( + view: widget.view, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart b/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart index 46236956..68605e31 100644 --- a/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart +++ b/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart @@ -36,7 +36,7 @@ class LocationFetchers extends ChangeNotifier { } void remove(final TaskView view) { - final fetcher = _findFetcher(view); + final fetcher = findFetcher(view); if (fetcher != null) { fetcher.dispose(); @@ -58,11 +58,11 @@ class LocationFetchers extends ChangeNotifier { } } - Fetcher? _findFetcher(final TaskView view) { + Fetcher? findFetcher(final TaskView view) { return _fetchers.firstWhereOrNull((fetcher) => fetcher.view == view); } List getLocations(final TaskView view) { - return _findFetcher(view)?.locations ?? []; + return findFetcher(view)?.locations ?? []; } } diff --git a/lib/screens/locations_overview_screen_widgets/ViewDetails.dart b/lib/screens/locations_overview_screen_widgets/ViewDetails.dart index 7b04e79c..c7f70590 100644 --- a/lib/screens/locations_overview_screen_widgets/ViewDetails.dart +++ b/lib/screens/locations_overview_screen_widgets/ViewDetails.dart @@ -24,7 +24,7 @@ import '../../utils/theme.dart'; import '../../widgets/BentoGridElement.dart'; import '../../widgets/LocusFlutterMap.dart'; import '../../widgets/RequestLocationPermissionMixin.dart'; -import '../ViewDetailScreen.dart'; +import '../ViewDetailsScreen.dart'; class ViewDetails extends StatefulWidget { final TaskView? view; diff --git a/lib/screens/shares_overview_screen_widgets/ViewTile.dart b/lib/screens/shares_overview_screen_widgets/ViewTile.dart index 2e0f0d7e..e994cb75 100644 --- a/lib/screens/shares_overview_screen_widgets/ViewTile.dart +++ b/lib/screens/shares_overview_screen_widgets/ViewTile.dart @@ -9,7 +9,7 @@ import 'package:locus/widgets/PlatformPopup.dart'; import 'package:provider/provider.dart'; import '../../widgets/PlatformListTile.dart'; -import '../ViewDetailScreen.dart'; +import '../ViewDetailsScreen.dart'; class ViewTile extends StatelessWidget { final TaskView view; @@ -76,7 +76,7 @@ class ViewTile extends StatelessWidget { Navigator.of(context).push( NativePageRoute( context: context, - builder: (context) => ViewDetailScreen( + builder: (context) => ViewDetailsScreen( view: view, ), ), diff --git a/lib/screens/view_details_screen_widgets/LocationPointsList.dart b/lib/screens/view_details_screen_widgets/LocationPointsList.dart new file mode 100644 index 00000000..4a5fb1aa --- /dev/null +++ b/lib/screens/view_details_screen_widgets/LocationPointsList.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:locus/screens/locations_overview_screen_widgets/LocationFetchers.dart'; +import 'package:locus/screens/task_detail_screen_widgets/LocationDetails.dart'; +import 'package:locus/services/view_service.dart'; +import 'package:provider/provider.dart'; + +class LocationPointsList extends StatefulWidget { + final TaskView view; + + const LocationPointsList({ + super.key, + required this.view, + }); + + @override + State createState() => _LocationPointsListState(); +} + +class _LocationPointsListState extends State { + final ScrollController controller = ScrollController(); + + @override + void initState() { + super.initState(); + + final locationFetchers = context.read(); + final fetcher = locationFetchers.findFetcher(widget.view)!; + + fetcher.addListener(_rebuild); + controller.addListener(() { + if (fetcher.hasFetchedAllLocations) { + return; + } + + if (controller.position.atEdge) { + final isTop = controller.position.pixels == 0; + + if (!isTop) { + fetcher.fetchMoreLocations(); + } + } + }); + } + + @override + void dispose() { + final locationFetchers = context.read(); + final fetcher = locationFetchers.findFetcher(widget.view)!; + + fetcher.removeListener(_rebuild); + + super.dispose(); + } + + void _rebuild() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + final locationFetchers = context.watch(); + final fetcher = locationFetchers.findFetcher(widget.view)!; + final locations = fetcher.locations; + + return ListView.builder( + shrinkWrap: true, + controller: controller, + itemCount: locations.length + (fetcher.isLoading ? 1 : 0), + itemBuilder: (_, index) { + if (index == locations.length) { + return const Center( + child: SizedBox.square( + dimension: 20, + child: CircularProgressIndicator(), + ), + ); + } + + return LocationDetails( + location: locations[index], + isPreview: false, + ); + }, + ); + } +} diff --git a/lib/services/location_fetcher_service/Fetcher.dart b/lib/services/location_fetcher_service/Fetcher.dart index 36435248..a1ff1c09 100644 --- a/lib/services/location_fetcher_service/Fetcher.dart +++ b/lib/services/location_fetcher_service/Fetcher.dart @@ -12,6 +12,7 @@ class Fetcher extends ChangeNotifier { bool _isMounted = true; bool _isLoading = false; bool _hasFetchedPreviewLocations = false; + bool _hasFetchedAllLocations = false; List get locations => _locations.locations; @@ -19,10 +20,13 @@ class Fetcher extends ChangeNotifier { bool get hasFetchedPreviewLocations => _hasFetchedPreviewLocations; + bool get hasFetchedAllLocations => _hasFetchedAllLocations; + Fetcher(this.view); void _getLocations({ final DateTime? from, + final DateTime? until, final int? limit, final VoidCallback? onEmptyEnd, final VoidCallback? onEnd, @@ -34,6 +38,7 @@ class Fetcher extends ChangeNotifier { final unsubscriber = view.getLocations( limit: limit, + until: until, from: from, onLocationFetched: (location) { if (!_isMounted) { @@ -71,9 +76,17 @@ class Fetcher extends ChangeNotifier { _getLocations( from: DateTime.now().subtract(const Duration(hours: 24)), onEnd: () { + if (!_isMounted) { + return; + } + _hasFetchedPreviewLocations = true; }, onEmptyEnd: () { + if (!_isMounted) { + return; + } + _getLocations( limit: 1, onEnd: () { @@ -87,6 +100,24 @@ class Fetcher extends ChangeNotifier { ); } + void fetchMoreLocations([ + int limit = 100, + ]) { + final earliestLocation = _locations.locations.first; + + _getLocations( + limit: limit, + until: earliestLocation.createdAt, + onEmptyEnd: () { + if (!_isMounted) { + return; + } + + _hasFetchedAllLocations = true; + }, + ); + } + void fetchAllLocations() { _getLocations(); } diff --git a/lib/services/view_service.dart b/lib/services/view_service.dart index 993b8152..59db0de4 100644 --- a/lib/services/view_service.dart +++ b/lib/services/view_service.dart @@ -239,6 +239,7 @@ class TaskView extends ChangeNotifier with LocationBase { final VoidCallback? onEmptyEnd, int? limit, DateTime? from, + DateTime? until, }) => get_locations_api.getLocations( encryptionPassword: _encryptionPassword, @@ -249,6 +250,7 @@ class TaskView extends ChangeNotifier with LocationBase { onEmptyEnd: onEmptyEnd, from: from, limit: limit, + until: until, ); Future> getLocationsAsFuture({ From e30405344d8cf054eb3dffd62ad5694a11bdb876 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 9 Sep 2023 13:34:30 +0200 Subject: [PATCH 12/15] refactor: MapActionsContainer.dart --- lib/screens/LocationsOverviewScreen.dart | 110 +++++++------- lib/screens/ViewDetailsScreen.dart | 21 --- .../ViewAlarmSelectRadiusRegionScreen.dart | 141 ++++++++++-------- .../ViewLocationPointsScreen.dart | 98 ------------ lib/widgets/MapActionsContainer.dart | 30 ++++ 5 files changed, 164 insertions(+), 236 deletions(-) delete mode 100644 lib/screens/view_details_screen_widgets/ViewLocationPointsScreen.dart create mode 100644 lib/widgets/MapActionsContainer.dart diff --git a/lib/screens/LocationsOverviewScreen.dart b/lib/screens/LocationsOverviewScreen.dart index a3b82037..29e6479b 100644 --- a/lib/screens/LocationsOverviewScreen.dart +++ b/lib/screens/LocationsOverviewScreen.dart @@ -45,6 +45,7 @@ import 'package:locus/widgets/GoToMyLocationMapAction.dart'; import 'package:locus/widgets/LocationsMap.dart'; import 'package:locus/widgets/LocusFlutterMap.dart'; import 'package:locus/widgets/CompassMapAction.dart'; +import 'package:locus/widgets/MapActionsContainer.dart'; import 'package:locus/widgets/Paper.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; @@ -1328,69 +1329,62 @@ class _LocationsOverviewScreenState extends State final shades = getPrimaryColorShades(context); if (settings.getMapProvider() == MapProvider.openStreetMap) { - return Positioned( - // Add half the difference to center the button - right: FAB_MARGIN + diff / 2, - bottom: FAB_SIZE + - FAB_MARGIN + - (isCupertino(context) ? LARGE_SPACE : SMALL_SPACE), - child: Column( - children: [ - AnimatedScale( - scale: showDetailedLocations ? 1 : 0, - duration: - showDetailedLocations ? 1200.milliseconds : 100.milliseconds, - curve: showDetailedLocations ? Curves.elasticOut : Curves.easeIn, - child: Tooltip( - message: disableShowDetailedLocations - ? l10n.locationsOverview_mapAction_detailedLocations_show - : l10n.locationsOverview_mapAction_detailedLocations_hide, - preferBelow: false, - margin: const EdgeInsets.only(bottom: margin), - child: SizedBox.square( - dimension: dimension, - child: Center( - child: Paper( - width: null, - borderRadius: BorderRadius.circular(HUGE_SPACE), - padding: EdgeInsets.zero, - child: IconButton( - color: shades[400], - icon: Icon(disableShowDetailedLocations - ? MdiIcons.mapMarkerMultipleOutline - : MdiIcons.mapMarkerMultiple), - onPressed: () { - setState(() { - disableShowDetailedLocations = - !disableShowDetailedLocations; - }); - }, - ), + return MapActionsContainer( + children: [ + AnimatedScale( + scale: showDetailedLocations ? 1 : 0, + duration: + showDetailedLocations ? 1200.milliseconds : 100.milliseconds, + curve: showDetailedLocations ? Curves.elasticOut : Curves.easeIn, + child: Tooltip( + message: disableShowDetailedLocations + ? l10n.locationsOverview_mapAction_detailedLocations_show + : l10n.locationsOverview_mapAction_detailedLocations_hide, + preferBelow: false, + margin: const EdgeInsets.only(bottom: margin), + child: SizedBox.square( + dimension: dimension, + child: Center( + child: Paper( + width: null, + borderRadius: BorderRadius.circular(HUGE_SPACE), + padding: EdgeInsets.zero, + child: IconButton( + color: shades[400], + icon: Icon(disableShowDetailedLocations + ? MdiIcons.mapMarkerMultipleOutline + : MdiIcons.mapMarkerMultiple), + onPressed: () { + setState(() { + disableShowDetailedLocations = + !disableShowDetailedLocations; + }); + }, ), ), ), ), ), - const SizedBox(height: SMALL_SPACE), - CompassMapAction( - onAlignNorth: () { - flutterMapController!.rotate(0); - }, - mapController: flutterMapController!, - ), - const SizedBox(height: SMALL_SPACE), - GoToMyLocationMapAction( - animate: locationStatus == LocationStatus.fetching, - onGoToMyLocation: () { - updateCurrentPosition( - askPermissions: true, - goToPosition: true, - showErrorMessage: true, - ); - }, - ), - ], - ), + ), + const SizedBox(height: SMALL_SPACE), + CompassMapAction( + onAlignNorth: () { + flutterMapController!.rotate(0); + }, + mapController: flutterMapController!, + ), + const SizedBox(height: SMALL_SPACE), + GoToMyLocationMapAction( + animate: locationStatus == LocationStatus.fetching, + onGoToMyLocation: () { + updateCurrentPosition( + askPermissions: true, + goToPosition: true, + showErrorMessage: true, + ); + }, + ), + ], ); } diff --git a/lib/screens/ViewDetailsScreen.dart b/lib/screens/ViewDetailsScreen.dart index 355d89c8..700c3e66 100644 --- a/lib/screens/ViewDetailsScreen.dart +++ b/lib/screens/ViewDetailsScreen.dart @@ -1,45 +1,24 @@ -import 'dart:async'; - -import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart' hide PlatformListTile; -import 'package:geolocator/geolocator.dart'; import 'package:locus/screens/view_alarm_screen_widgets/ViewAlarmScreen.dart'; import 'package:locus/screens/view_details_screen_widgets/LocationPointsList.dart'; -import 'package:locus/screens/view_details_screen_widgets/ViewLocationPointsScreen.dart'; -import 'package:locus/services/location_alarm_service.dart'; import 'package:locus/services/view_service.dart'; import 'package:locus/utils/PageRoute.dart'; -import 'package:locus/utils/bunny.dart'; -import 'package:locus/utils/permissions/has-granted.dart'; -import 'package:locus/utils/permissions/request.dart'; -import 'package:locus/widgets/EmptyLocationsThresholdScreen.dart'; -import 'package:locus/widgets/FillUpPaint.dart'; -import 'package:locus/widgets/LocationFetchEmpty.dart'; -import 'package:locus/widgets/LocationsMap.dart'; import 'package:locus/widgets/OpenInMaps.dart'; import 'package:locus/widgets/Paper.dart'; import 'package:locus/widgets/PlatformFlavorWidget.dart'; import 'package:locus/widgets/PlatformPopup.dart'; -import 'package:map_launcher/map_launcher.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:provider/provider.dart'; import '../constants/spacing.dart'; -import '../services/location_fetch_controller.dart'; -import '../services/location_point_service.dart'; import '../utils/theme.dart'; -import '../widgets/LocationFetchError.dart'; -import '../widgets/LocationStillFetchingBanner.dart'; -import '../widgets/LocationsLoadingScreen.dart'; import '../widgets/PlatformListTile.dart'; import 'locations_overview_screen_widgets/LocationFetchers.dart'; -const DEBOUNCE_DURATION = Duration(seconds: 2); - class ViewDetailsScreen extends StatefulWidget { final TaskView view; diff --git a/lib/screens/view_alarm_screen_widgets/ViewAlarmSelectRadiusRegionScreen.dart b/lib/screens/view_alarm_screen_widgets/ViewAlarmSelectRadiusRegionScreen.dart index 3563dba9..7d67d25e 100644 --- a/lib/screens/view_alarm_screen_widgets/ViewAlarmSelectRadiusRegionScreen.dart +++ b/lib/screens/view_alarm_screen_widgets/ViewAlarmSelectRadiusRegionScreen.dart @@ -9,6 +9,7 @@ import 'package:geolocator/geolocator.dart'; import 'package:latlong2/latlong.dart'; import 'package:locus/constants/spacing.dart'; import 'package:locus/constants/values.dart'; +import 'package:locus/screens/locations_overview_screen_widgets/constants.dart'; import 'package:locus/screens/view_alarm_screen_widgets/RadiusRegionMetaDataSheet.dart'; import 'package:locus/services/location_alarm_service.dart'; import 'package:locus/services/settings_service/index.dart'; @@ -18,7 +19,9 @@ import 'package:locus/utils/location/index.dart'; import 'package:locus/utils/permissions/has-granted.dart'; import 'package:locus/utils/permissions/request.dart'; import 'package:locus/utils/theme.dart'; +import 'package:locus/widgets/GoToMyLocationMapAction.dart'; import 'package:locus/widgets/LocationsMap.dart'; +import 'package:locus/widgets/MapActionsContainer.dart'; import 'package:locus/widgets/RequestNotificationPermissionMixin.dart'; import 'package:provider/provider.dart'; import 'package:shimmer/shimmer.dart'; @@ -48,6 +51,8 @@ class _ViewAlarmSelectRadiusRegionScreenState double previousScale = 1; Stream? _positionStream; + bool isGoingToCurrentPosition = false; + @override void initState() { super.initState(); @@ -79,34 +84,36 @@ class _ViewAlarmSelectRadiusRegionScreenState showHelperSheet( context: context, - builder: (context) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(l10n.location_addAlarm_radiusBased_help_description), - const SizedBox(height: MEDIUM_SPACE), - Row( + builder: (context) => + Column( + mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.touch_app_rounded), - const SizedBox(width: MEDIUM_SPACE), - Flexible( - child: Text( - l10n.location_addAlarm_radiusBased_help_tapDescription), + Text(l10n.location_addAlarm_radiusBased_help_description), + const SizedBox(height: MEDIUM_SPACE), + Row( + children: [ + const Icon(Icons.touch_app_rounded), + const SizedBox(width: MEDIUM_SPACE), + Flexible( + child: Text( + l10n.location_addAlarm_radiusBased_help_tapDescription), + ), + ], ), - ], - ), - const SizedBox(height: MEDIUM_SPACE), - Row( - children: [ - const Icon(Icons.pinch_rounded), - const SizedBox(width: MEDIUM_SPACE), - Flexible( - child: Text( - l10n.location_addAlarm_radiusBased_help_pinchDescription), + const SizedBox(height: MEDIUM_SPACE), + Row( + children: [ + const Icon(Icons.pinch_rounded), + const SizedBox(width: MEDIUM_SPACE), + Flexible( + child: Text( + l10n + .location_addAlarm_radiusBased_help_pinchDescription), + ), + ], ), ], ), - ], - ), title: l10n.location_addAlarm_radiusBased_help_title, sheetName: HelperSheet.radiusBasedAlarms, ); @@ -117,8 +124,16 @@ class _ViewAlarmSelectRadiusRegionScreenState return; } + setState(() { + isGoingToCurrentPosition = true; + }); + _positionStream = getLastAndCurrentPosition() ..listen((position) { + setState(() { + isGoingToCurrentPosition = false; + }); + flutterMapController?.move( LatLng(position.latitude, position.longitude), 13, @@ -139,7 +154,8 @@ class _ViewAlarmSelectRadiusRegionScreenState super.dispose(); } - CircleLayer getFlutterMapCircleLayer() => CircleLayer( + CircleLayer getFlutterMapCircleLayer() => + CircleLayer( circles: [ CircleMarker( point: alarmCenter!, @@ -160,14 +176,15 @@ class _ViewAlarmSelectRadiusRegionScreenState isDismissible: true, isScrollControlled: true, ), - builder: (_) => RadiusRegionMetaDataSheet( - center: alarmCenter!, - radius: radius, - ), + builder: (_) => + RadiusRegionMetaDataSheet( + center: alarmCenter!, + radius: radius, + ), ); final hasGrantedNotificationAccess = - await showNotificationPermissionDialog(); + await showNotificationPermissionDialog(); if (!hasGrantedNotificationAccess) { return; @@ -293,53 +310,54 @@ class _ViewAlarmSelectRadiusRegionScreenState ); } + Widget buildMapActions() { + return MapActionsContainer( + children: [ + GoToMyLocationMapAction( + onGoToMyLocation: goToCurrentPosition, + animate: isGoingToCurrentPosition, + ), + ], + ); + } + @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); return PlatformScaffold( - material: (_, __) => MaterialScaffoldData( - resizeToAvoidBottomInset: false, - ), + material: (_, __) => + MaterialScaffoldData( + resizeToAvoidBottomInset: false, + ), appBar: PlatformAppBar( title: Text(l10n.location_addAlarm_radiusBased_title), trailingActions: [ PlatformIconButton( - cupertino: (_, __) => CupertinoIconButtonData( - padding: EdgeInsets.zero, - ), - icon: const Icon(Icons.my_location_rounded), - onPressed: () async { - final hasGrantedLocation = await requestBasicLocationPermission(); - - if (hasGrantedLocation) { - goToCurrentPosition(); - } - }, - ), - PlatformIconButton( - cupertino: (_, __) => CupertinoIconButtonData( - padding: EdgeInsets.zero, - ), + cupertino: (_, __) => + CupertinoIconButtonData( + padding: EdgeInsets.zero, + ), icon: Icon(context.platformIcons.help), onPressed: showHelp, ), ], - cupertino: (_, __) => CupertinoNavigationBarData( - backgroundColor: isInScaleMode - ? null - : getCupertinoAppBarColorForMapScreen(context), - ), + cupertino: (_, __) => + CupertinoNavigationBarData( + backgroundColor: isInScaleMode + ? null + : getCupertinoAppBarColorForMapScreen(context), + ), ), body: GestureDetector( onScaleUpdate: isInScaleMode ? updateZoom : null, onTap: isInScaleMode ? () { - Vibration.vibrate(duration: 50); - setState(() { - isInScaleMode = false; - }); - } + Vibration.vibrate(duration: 50); + setState(() { + isInScaleMode = false; + }); + } : null, // We need a `Stack` to disable the map, but also need to show a container to detect the long press again child: Column( @@ -352,7 +370,12 @@ class _ViewAlarmSelectRadiusRegionScreenState Positioned.fill( child: IgnorePointer( ignoring: isInScaleMode, - child: buildMap(), + child: Stack( + children: [ + buildMap(), + buildMapActions(), + ], + ), ), ), if (isInScaleMode) ...[ diff --git a/lib/screens/view_details_screen_widgets/ViewLocationPointsScreen.dart b/lib/screens/view_details_screen_widgets/ViewLocationPointsScreen.dart deleted file mode 100644 index 40a68100..00000000 --- a/lib/screens/view_details_screen_widgets/ViewLocationPointsScreen.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:locus/constants/spacing.dart'; -import 'package:locus/services/location_fetch_controller.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import '../task_detail_screen_widgets/LocationDetails.dart'; - -class ViewLocationPointsScreen extends StatefulWidget { - final LocationFetcher locationFetcher; - - const ViewLocationPointsScreen({ - required this.locationFetcher, - super.key, - }); - - @override - State createState() => _ViewLocationPointsScreenState(); -} - -class _ViewLocationPointsScreenState extends State { - final ScrollController _controller = ScrollController(); - - @override - void initState() { - super.initState(); - - widget.locationFetcher.addListener(updateView); - - _controller.addListener(() { - print(widget.locationFetcher.canFetchMore); - if (!widget.locationFetcher.canFetchMore) { - return; - } - - if (_controller.position.atEdge) { - final isTop = _controller.position.pixels == 0; - - if (!isTop) { - widget.locationFetcher.fetchMore(onEnd: () { - setState(() {}); - }); - } - } - }); - } - - updateView() { - setState(() {}); - } - - @override - void dispose() { - widget.locationFetcher.removeListener(updateView); - _controller.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context); - - return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(l10n.locationPointsScreen_title), - material: (_, __) => MaterialAppBarData( - centerTitle: true, - ), - ), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(MEDIUM_SPACE), - child: ListView.builder( - shrinkWrap: true, - controller: _controller, - itemCount: widget.locationFetcher.controller.locations.length + (widget.locationFetcher.isLoading ? 1 : 0), - itemBuilder: (_, index) { - if (index == widget.locationFetcher.controller.locations.length) { - return const Center( - child: SizedBox.square( - dimension: 20, - child: CircularProgressIndicator(), - ), - ); - } - - return LocationDetails( - location: widget.locationFetcher.controller.locations[index], - isPreview: false, - ); - }, - ), - ), - ), - ); - } -} diff --git a/lib/widgets/MapActionsContainer.dart b/lib/widgets/MapActionsContainer.dart new file mode 100644 index 00000000..fba19fd2 --- /dev/null +++ b/lib/widgets/MapActionsContainer.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:locus/constants/spacing.dart'; +import 'package:locus/screens/locations_overview_screen_widgets/constants.dart'; + +const MAP_ACTION_SIZE = 50.0; +const diff = FAB_SIZE - MAP_ACTION_SIZE; + +class MapActionsContainer extends StatelessWidget { + final List children; + + const MapActionsContainer({ + super.key, + required this.children, + }); + + @override + Widget build(BuildContext context) { + return Positioned( + // Add half the difference to center the button + right: FAB_MARGIN + diff / 2, + bottom: FAB_SIZE + + FAB_MARGIN + + (isCupertino(context) ? LARGE_SPACE : SMALL_SPACE), + child: Column( + children: children, + ), + ); + } +} From bc2f9ba704cdf30d62e3b6a6c40adc2404f6fa64 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:13:44 +0200 Subject: [PATCH 13/15] fix: Use task name as title in TaskDetailScreen.dart --- lib/screens/TaskDetailScreen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/TaskDetailScreen.dart b/lib/screens/TaskDetailScreen.dart index 8819103e..e5dc499a 100644 --- a/lib/screens/TaskDetailScreen.dart +++ b/lib/screens/TaskDetailScreen.dart @@ -108,7 +108,7 @@ class _TaskDetailScreenState extends State { return PlatformScaffold( appBar: PlatformAppBar( - title: Text(l10n.taskDetails_title), + title: Text(widget.task.name), material: (_, __) => MaterialAppBarData( centerTitle: true, ), From b422b5fac376d244269f96dd58eb5acfb27c99ae Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:57:34 +0200 Subject: [PATCH 14/15] fix: Improve location fetchers --- lib/screens/LocationsOverviewScreen.dart | 8 +-- lib/screens/TaskDetailScreen.dart | 2 - .../LocationFetchers.dart | 2 +- .../ViewDetails.dart | 56 ++++++++++--------- .../LocationDetails.dart | 5 +- .../LocationPointsList.dart | 8 ++- .../location_fetcher_service/Fetcher.dart | 12 ++-- .../location_fetcher_service/Locations.dart | 8 ++- lib/services/location_point_service.dart | 42 +++++++++----- 9 files changed, 85 insertions(+), 58 deletions(-) diff --git a/lib/screens/LocationsOverviewScreen.dart b/lib/screens/LocationsOverviewScreen.dart index 29e6479b..aaec068e 100644 --- a/lib/screens/LocationsOverviewScreen.dart +++ b/lib/screens/LocationsOverviewScreen.dart @@ -742,8 +742,8 @@ class _LocationsOverviewScreenState extends State final Iterable<(TaskView, LocationPointService)> circleLocations = selectedViewID == null ? locationFetchers.fetchers - .where((fetcher) => fetcher.locations.isNotEmpty) - .map((fetcher) => (fetcher.view, fetcher.locations.last)) + .where((fetcher) => fetcher.sortedLocations.isNotEmpty) + .map((fetcher) => (fetcher.view, fetcher.sortedLocations.last)) : viewService.views .map( (view) => mergeLocationsIfRequired( @@ -983,10 +983,10 @@ class _LocationsOverviewScreenState extends State children: locationFetchers.fetchers .where((fetcher) => (selectedViewID == null || fetcher.view.id == selectedViewID) && - fetcher.locations.isNotEmpty) + fetcher.sortedLocations.isNotEmpty) .map( (fetcher) => OutOfBoundMarker( - lastViewLocation: fetcher.locations.last, + lastViewLocation: fetcher.sortedLocations.last, onTap: () { showViewLocations(fetcher.view); }, diff --git a/lib/screens/TaskDetailScreen.dart b/lib/screens/TaskDetailScreen.dart index e5dc499a..06d8a89c 100644 --- a/lib/screens/TaskDetailScreen.dart +++ b/lib/screens/TaskDetailScreen.dart @@ -104,8 +104,6 @@ class _TaskDetailScreenState extends State { @override Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context); - return PlatformScaffold( appBar: PlatformAppBar( title: Text(widget.task.name), diff --git a/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart b/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart index 68605e31..28687745 100644 --- a/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart +++ b/lib/screens/locations_overview_screen_widgets/LocationFetchers.dart @@ -63,6 +63,6 @@ class LocationFetchers extends ChangeNotifier { } List getLocations(final TaskView view) { - return findFetcher(view)?.locations ?? []; + return findFetcher(view)?.sortedLocations ?? []; } } diff --git a/lib/screens/locations_overview_screen_widgets/ViewDetails.dart b/lib/screens/locations_overview_screen_widgets/ViewDetails.dart index c7f70590..a8eb5957 100644 --- a/lib/screens/locations_overview_screen_widgets/ViewDetails.dart +++ b/lib/screens/locations_overview_screen_widgets/ViewDetails.dart @@ -54,9 +54,7 @@ class _ViewDetailsState extends State { oldLastLocation = oldWidget.location; } - Widget buildHeadingMap( - final LocationPointService lastLocation, - ) { + Widget buildHeadingMap(final LocationPointService lastLocation,) { return ClipRRect( borderRadius: BorderRadius.circular(LARGE_SPACE), child: SizedBox( @@ -77,14 +75,15 @@ class _ViewDetailsState extends State { lastLocation.latitude, lastLocation.longitude, ), - builder: (context) => Transform.rotate( - angle: lastLocation.heading!, - child: Icon( - CupertinoIcons.location_north_fill, - color: getPrimaryColorShades(context)[0], - size: 30, - ), - ), + builder: (context) => + Transform.rotate( + angle: lastLocation.heading!, + child: Icon( + CupertinoIcons.location_north_fill, + color: getPrimaryColorShades(context)[0], + size: 30, + ), + ), ), ], ) @@ -123,8 +122,8 @@ class _ViewDetailsState extends State { title: lastLocation.altitude == null ? l10n.unknownValue : l10n.locations_values_altitude_m( - lastLocation.altitude!.round(), - ), + lastLocation.altitude!.round(), + ), icon: platformThemeData( context, material: (_) => Icons.height_rounded, @@ -137,8 +136,8 @@ class _ViewDetailsState extends State { title: lastLocation.speed == null ? l10n.unknownValue : l10n.locations_values_speed_kmh( - (lastLocation.speed! * 3.6).round(), - ), + (lastLocation.speed! * 3.6).round(), + ), icon: platformThemeData( context, material: (_) => Icons.speed, @@ -151,8 +150,8 @@ class _ViewDetailsState extends State { title: lastLocation.batteryLevel == null ? l10n.unknownValue : l10n.locations_values_battery_value( - (lastLocation.batteryLevel! * 100).round(), - ), + (lastLocation.batteryLevel! * 100).round(), + ), icon: getIconDataForBatteryLevel( context, lastLocation.batteryLevel, @@ -164,8 +163,8 @@ class _ViewDetailsState extends State { title: lastLocation.batteryState == null ? l10n.unknownValue : l10n.locations_values_batteryState_value( - lastLocation.batteryState!.name, - ), + lastLocation.batteryState!.name, + ), icon: Icons.cable_rounded, type: BentoType.tertiary, description: l10n.locations_values_batteryState_description, @@ -202,6 +201,10 @@ class _DistanceBentoElementState extends State void fetchCurrentPosition() async { _positionStream = getLastAndCurrentPosition(updateLocation: true) ..listen((position) { + if (!mounted) { + return; + } + setState(() { currentPosition = position; }); @@ -239,9 +242,10 @@ class _DistanceBentoElementState extends State showPlatformModalSheet( context: context, material: MaterialModalSheetData(), - builder: (context) => OpenInMaps( - destination: widget.lastLocation.asCoords(), - ), + builder: (context) => + OpenInMaps( + destination: widget.lastLocation.asCoords(), + ), ); }, title: (() { @@ -338,10 +342,10 @@ class _LastLocationBentoElementState extends State { title: showAbsolute ? formatDateTimeHumanReadable(widget.lastLocation.createdAt) : GetTimeAgo.parse( - DateTime.now().subtract( - DateTime.now().difference(widget.lastLocation.createdAt), - ), - ), + DateTime.now().subtract( + DateTime.now().difference(widget.lastLocation.createdAt), + ), + ), icon: Icons.location_on_rounded, description: l10n.locations_values_lastLocation_description, ); diff --git a/lib/screens/task_detail_screen_widgets/LocationDetails.dart b/lib/screens/task_detail_screen_widgets/LocationDetails.dart index 8e65f4a2..8e4d6b19 100644 --- a/lib/screens/task_detail_screen_widgets/LocationDetails.dart +++ b/lib/screens/task_detail_screen_widgets/LocationDetails.dart @@ -8,6 +8,7 @@ import 'package:flutter_platform_widgets/flutter_platform_widgets.dart' import 'package:locus/constants/spacing.dart'; import 'package:locus/services/location_point_service.dart'; import 'package:locus/services/settings_service/index.dart'; +import 'package:locus/utils/date.dart'; import 'package:locus/utils/icon.dart'; import 'package:locus/utils/theme.dart'; import 'package:locus/utils/ui-message/show-message.dart'; @@ -90,9 +91,7 @@ class _LocationDetailsState extends State { child: Align( alignment: Alignment.centerLeft, child: Text( - l10n.taskDetails_locationDetails_createdAt_value( - widget.location.createdAt, - ), + formatDateTimeHumanReadable(widget.location.createdAt), textAlign: TextAlign.start, style: getBodyTextTextStyle(context), ), diff --git a/lib/screens/view_details_screen_widgets/LocationPointsList.dart b/lib/screens/view_details_screen_widgets/LocationPointsList.dart index 4a5fb1aa..a98e49a3 100644 --- a/lib/screens/view_details_screen_widgets/LocationPointsList.dart +++ b/lib/screens/view_details_screen_widgets/LocationPointsList.dart @@ -40,6 +40,10 @@ class _LocationPointsListState extends State { } } }); + + if (!fetcher.hasFetchedAllLocations) { + fetcher.fetchMoreLocations(); + } } @override @@ -60,7 +64,9 @@ class _LocationPointsListState extends State { Widget build(BuildContext context) { final locationFetchers = context.watch(); final fetcher = locationFetchers.findFetcher(widget.view)!; - final locations = fetcher.locations; + final locations = fetcher.isLoading + ? fetcher.sortedLocations + : fetcher.locations.toList(); return ListView.builder( shrinkWrap: true, diff --git a/lib/services/location_fetcher_service/Fetcher.dart b/lib/services/location_fetcher_service/Fetcher.dart index a1ff1c09..ae76bfc8 100644 --- a/lib/services/location_fetcher_service/Fetcher.dart +++ b/lib/services/location_fetcher_service/Fetcher.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:flutter/foundation.dart'; import 'package:locus/services/location_fetcher_service/Locations.dart'; import 'package:locus/services/location_point_service.dart'; @@ -14,7 +16,10 @@ class Fetcher extends ChangeNotifier { bool _hasFetchedPreviewLocations = false; bool _hasFetchedAllLocations = false; - List get locations => _locations.locations; + UnmodifiableSetView get locations => + _locations.locations; + + List get sortedLocations => _locations.sortedLocations; bool get isLoading => _isLoading; @@ -47,7 +52,6 @@ class Fetcher extends ChangeNotifier { _locations.add(location); onLocationFetched?.call(location); - notifyListeners(); }, onEnd: () { if (!_isMounted) { @@ -101,9 +105,9 @@ class Fetcher extends ChangeNotifier { } void fetchMoreLocations([ - int limit = 100, + int limit = 50, ]) { - final earliestLocation = _locations.locations.first; + final earliestLocation = _locations.sortedLocations.first; _getLocations( limit: limit, diff --git a/lib/services/location_fetcher_service/Locations.dart b/lib/services/location_fetcher_service/Locations.dart index 2686452f..b7846c9a 100644 --- a/lib/services/location_fetcher_service/Locations.dart +++ b/lib/services/location_fetcher_service/Locations.dart @@ -5,8 +5,12 @@ import 'package:locus/services/location_point_service.dart'; class Locations { final Set _locations = {}; - List get locations => _locations.toList(growable: false) - ..sort((a, b) => a.createdAt.compareTo(b.createdAt)); + List get sortedLocations => + _locations.toList(growable: false) + ..sort((a, b) => a.createdAt.compareTo(b.createdAt)); + + UnmodifiableSetView get locations => + UnmodifiableSetView(_locations); Locations(); diff --git a/lib/services/location_point_service.dart b/lib/services/location_point_service.dart index cfd75e87..043d584e 100644 --- a/lib/services/location_point_service.dart +++ b/lib/services/location_point_service.dart @@ -41,18 +41,31 @@ class LocationPointService { double? batteryLevel, this.isCopy = false, this.batteryState, - }) : altitude = altitude == 0.0 ? null : altitude, + }) + : altitude = altitude == 0.0 ? null : altitude, speed = speed == 0.0 ? null : speed, speedAccuracy = speedAccuracy == 0.0 ? null : speedAccuracy, heading = heading == 0.0 ? null : heading, headingAccuracy = headingAccuracy == 0.0 ? null : headingAccuracy, batteryLevel = batteryLevel == 0.0 ? null : batteryLevel; + @override + int get hashCode => Object.hash(id, 0); + + @override + bool operator ==(Object other) { + if (other is LocationPointService) { + return id == other.id; + } + + return false; + } + String formatRawAddress() => "${latitude.toStringAsFixed(5)}, ${longitude.toStringAsFixed(5)}"; factory LocationPointService.dummyFromLatLng(final LatLng latLng, - {final double accuracy = 10.0}) => + {final double accuracy = 10.0}) => LocationPointService( id: uuid.v4(), createdAt: DateTime.now(), @@ -62,9 +75,9 @@ class LocationPointService { ); static Future fromLocationDto( - final LocationDto locationDto, [ - final bool addBatteryInfo = true, - ]) async { + final LocationDto locationDto, [ + final bool addBatteryInfo = true, + ]) async { BatteryInfo? batteryInfo; if (addBatteryInfo) { @@ -112,8 +125,8 @@ class LocationPointService { batteryState: json["batteryState"] == null ? null : BatteryState.values.firstWhere( - (value) => value.name == json["batteryState"], - ), + (value) => value.name == json["batteryState"], + ), ); } @@ -135,8 +148,7 @@ class LocationPointService { } static Future fromPosition( - final Position position, - ) async { + final Position position,) async { BatteryInfo? batteryInfo; try { @@ -166,7 +178,8 @@ class LocationPointService { /// Copies `current` with a new id - mainly used in conjunction with `createUsingCurrentLocation` /// in background fetch to avoid fetching the location multiple times. - LocationPointService copyWithDifferentId() => LocationPointService( + LocationPointService copyWithDifferentId() => + LocationPointService( id: uuid.v4(), createdAt: DateTime.now(), latitude: latitude, @@ -180,10 +193,8 @@ class LocationPointService { batteryState: batteryState, ); - static Future fromEncrypted( - final String cipherText, - final SecretKey encryptionPassword, - ) async { + static Future fromEncrypted(final String cipherText, + final SecretKey encryptionPassword,) async { final message = await decryptUsingAES( cipherText, encryptionPassword, @@ -192,7 +203,8 @@ class LocationPointService { return LocationPointService.fromJSON(jsonDecode(message)); } - Position asPosition() => Position( + Position asPosition() => + Position( latitude: latitude, longitude: longitude, altitude: altitude ?? 0.0, From d5449fc12a0e1a6f687e0aa4c33df1e0e98eefb4 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 9 Sep 2023 15:08:30 +0200 Subject: [PATCH 15/15] fix: Improve location circle preview --- lib/screens/LocationsOverviewScreen.dart | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/screens/LocationsOverviewScreen.dart b/lib/screens/LocationsOverviewScreen.dart index aaec068e..06f9557c 100644 --- a/lib/screens/LocationsOverviewScreen.dart +++ b/lib/screens/LocationsOverviewScreen.dart @@ -737,8 +737,6 @@ class _LocationsOverviewScreenState extends State final viewService = context.read(); final locationFetchers = context.read(); - final shades = getPrimaryColorShades(context); - final Iterable<(TaskView, LocationPointService)> circleLocations = selectedViewID == null ? locationFetchers.fetchers @@ -842,6 +840,7 @@ class _LocationsOverviewScreenState extends State } final lastCenter = settings.getLastMapLocation()?.toLatLng(); + final colorOpacityMultiplier = selectedViewID == null ? 1.0 : .1; return LocusFlutterMap( mapController: flutterMapController, options: MapOptions( @@ -862,8 +861,8 @@ class _LocationsOverviewScreenState extends State useRadiusInMeter: true, point: LatLng(location.latitude, location.longitude), borderStrokeWidth: 1, - color: view.color.withOpacity(.1), - borderColor: view.color, + color: view.color.withOpacity(.1 * colorOpacityMultiplier), + borderColor: view.color.withOpacity(colorOpacityMultiplier), ); }) .toList() @@ -879,9 +878,9 @@ class _LocationsOverviewScreenState extends State visibleLocation!.latitude, visibleLocation!.longitude, ), - borderStrokeWidth: 3, - color: shades[500]!.withOpacity(.3), - borderColor: shades[500]!, + borderStrokeWidth: 5, + color: selectedView!.color.withOpacity(.2), + borderColor: selectedView!.color, ) ], ),