diff --git a/.fvmrc b/.fvmrc index c2783c6..a8ea851 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.27.4" + "flutter": "3.38.9" } \ No newline at end of file diff --git a/.github/workflows/melos-ci.yml b/.github/workflows/melos-ci.yml index 5ec18be..131c52d 100644 --- a/.github/workflows/melos-ci.yml +++ b/.github/workflows/melos-ci.yml @@ -11,5 +11,5 @@ jobs: secrets: inherit permissions: write-all with: - subfolder: '.' # add optional subfolder to run workflow in - flutter_version: 3.27.4 + subfolder: "." # add optional subfolder to run workflow in + flutter_version: 3.38.9 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4e5f0ea..1ddf7f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,11 +4,11 @@ name: Iconica Standard Component Release Workflow on: release: types: [published] - + workflow_dispatch: jobs: call-global-iconica-workflow: uses: Iconica-Development/.github/.github/workflows/component-release.yml@master secrets: inherit - permissions: write-all \ No newline at end of file + permissions: write-all diff --git a/README.md b/README.md index 3761149..acc67d5 100644 --- a/README.md +++ b/README.md @@ -27,16 +27,10 @@ So before setting it to false you need to provide a google maps API key in the A ```dart LocationMapOptions( enableOpenMapsTileLayer: true, - tileProvider: CancellableNetworkTileProvider(), + tileProvider: NetworkTileProvider(), ) ``` -When using the openmaps tilelayer from the flutter_maps package it is recommended to use the [CancellableNetworkTileProvider](https://pub.dev/packages/flutter_map_cancellable_tile_provider) especially on Web where performance is drastically improved. Because flutter_locations shouldn't depend on flutter_map_cancellable_tile_provider it is not included in the dependencies. You can add it to your pubspec.yaml file like this: - -```yaml -dependencies: - flutter_map_cancellable_tile_provider: ^latest_version -``` For the features of this userstory permissions are required for each platform. diff --git a/apps/example/.gitignore b/apps/example/.gitignore index 47f681d..ab688f9 100644 --- a/apps/example/.gitignore +++ b/apps/example/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/apps/example/lib/main.dart b/apps/example/lib/main.dart index 94f5d72..840358d 100644 --- a/apps/example/lib/main.dart +++ b/apps/example/lib/main.dart @@ -1,17 +1,13 @@ import "package:device_preview/device_preview.dart"; import "package:flutter/material.dart"; import "package:flutter_locations/flutter_locations.dart"; -import "package:flutter_map_cancellable_tile_provider/flutter_map_cancellable_tile_provider.dart"; void main() { runApp( DevicePreview( enabled: true, isToolbarVisible: true, - availableLocales: const [ - Locale("en_US"), - Locale("nl_NL"), - ], + availableLocales: const [Locale("en_US"), Locale("nl_NL")], builder: (_) => const App(), ), ); @@ -22,28 +18,25 @@ class App extends StatelessWidget { @override Widget build(BuildContext context) => MaterialApp( - builder: DevicePreview.appBuilder, - locale: DevicePreview.locale(context), - supportedLocales: const [ - Locale("en", "US"), - Locale("nl", "NL"), - ], - theme: ThemeData.light(), - darkTheme: ThemeData.dark(), - home: LocationsUserStory( - options: LocationsOptions( - respositoryInterface: LocationsLocalRepository(density: 7), - mapOptions: LocationsMapOptions( - zoom: 7, - initialLocation: const Location( - latitude: 52.2056435, - longitude: 5.2, - ), - // Openmaps works without an API key - enableOpenMapsTileLayer: true, - tileProvider: CancellableNetworkTileProvider(), - ), + builder: DevicePreview.appBuilder, + locale: DevicePreview.locale(context), + supportedLocales: const [Locale("en", "US"), Locale("nl", "NL")], + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + home: LocationsUserStory( + options: LocationsOptions( + respositoryInterface: LocationsLocalRepository(density: 7), + mapOptions: LocationsMapOptions( + zoom: 7, + initialLocation: const Location(latitude: 52.2056435, longitude: 5.2), + // Open street maps works without an API key + enableOpenMapsTileLayer: true, + tileProvider: NetworkTileProvider( + // Open street maps requires a useragent + headers: {"User-Agent": "TempApp / 0.5"}, ), ), - ); + ), + ), + ); } diff --git a/apps/example/pubspec.yaml b/apps/example/pubspec.yaml index 1c631e8..938363c 100644 --- a/apps/example/pubspec.yaml +++ b/apps/example/pubspec.yaml @@ -3,7 +3,7 @@ description: "Flutter Locations userstory example app" version: 1.0.0+1 environment: - sdk: ">=3.4.3 <4.0.0" + sdk: ">=3.8.0 <4.0.0" dependencies: device_preview: ^1.2.0 @@ -12,7 +12,6 @@ dependencies: flutter_locations: hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub version: ^1.0.0 - flutter_map_cancellable_tile_provider: ^3.0.2 dev_dependencies: flutter_test: diff --git a/packages/flutter_locations/lib/flutter_locations.dart b/packages/flutter_locations/lib/flutter_locations.dart index ffcb1e2..51d2109 100644 --- a/packages/flutter_locations/lib/flutter_locations.dart +++ b/packages/flutter_locations/lib/flutter_locations.dart @@ -2,9 +2,13 @@ library flutter_locations; export "package:dart_locations_repository_interface/dart_locations_repository_interface.dart"; +export "package:flutter_map/flutter_map.dart"; export "src/config/locations_options.dart"; export "src/config/locations_translations.dart"; export "src/config/map/controls_options.dart"; export "src/config/map/map_options.dart"; +export "src/ui/widgets/home.dart"; +export "src/ui/widgets/map/map.dart"; export "src/userstories.dart"; +export "src/util/scope.dart"; diff --git a/packages/flutter_locations/lib/src/config/locations_options.dart b/packages/flutter_locations/lib/src/config/locations_options.dart index 85a3bbd..1e395f4 100644 --- a/packages/flutter_locations/lib/src/config/locations_options.dart +++ b/packages/flutter_locations/lib/src/config/locations_options.dart @@ -1,3 +1,4 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first import "package:flutter/material.dart"; import "package:flutter_locations/flutter_locations.dart"; import "package:flutter_locations/src/config/list/list_options.dart"; @@ -29,6 +30,24 @@ class LocationsOptions { /// The list options. final LocationsListOptions listOptions; + + LocationsOptions copy(LocationsOptions Function(LocationsOptions old) copy) => + copy(this); + + LocationsOptions copyWith({ + LocationsRepositoryInterface? respositoryInterface, + LocationsTranslations? translations, + LocationsBaseScreenBuilder? builder, + LocationsMapOptions? mapOptions, + LocationsListOptions? listOptions, + }) => + LocationsOptions( + respositoryInterface: respositoryInterface ?? this.respositoryInterface, + translations: translations ?? this.translations, + builder: builder ?? this.builder, + mapOptions: mapOptions ?? this.mapOptions, + listOptions: listOptions ?? this.listOptions, + ); } /// diff --git a/packages/flutter_locations/lib/src/config/map/map_options.dart b/packages/flutter_locations/lib/src/config/map/map_options.dart index 503ce76..85a3f39 100644 --- a/packages/flutter_locations/lib/src/config/map/map_options.dart +++ b/packages/flutter_locations/lib/src/config/map/map_options.dart @@ -1,3 +1,4 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first import "package:dart_locations_repository_interface/dart_locations_repository_interface.dart"; import "package:flutter/material.dart"; import "package:flutter_locations/src/config/map/controls_options.dart"; @@ -9,6 +10,7 @@ import "package:flutter_map/flutter_map.dart"; class LocationsMapOptions { /// [LocationsMapOptions] constructor const LocationsMapOptions({ + this.controller, this.zoom, this.maxZoom = 18.0, this.minZoom = 3.0, @@ -17,6 +19,7 @@ class LocationsMapOptions { this.onMapReady, this.searchBuilder = DefaultLocationsMapSearch.builder, this.markerBuilder = DefaultLocationsMapMarker.builder, + this.markerClusterBuilder = DefaultLocationsMapMarkerCluster.builder, this.controlsOptions = const LocationsMapControlsOptions.empty(), this.enableOpenMapsTileLayer = false, this.tileProvider, @@ -24,6 +27,7 @@ class LocationsMapOptions { /// const LocationsMapOptions.empty({ + this.controller, this.additionalLayers = const [], this.initialLocation = const Location(latitude: 0, longitude: 0), this.zoom, @@ -32,11 +36,15 @@ class LocationsMapOptions { this.onMapReady, this.searchBuilder = DefaultLocationsMapSearch.builder, this.markerBuilder = DefaultLocationsMapMarker.builder, + this.markerClusterBuilder = DefaultLocationsMapMarkerCluster.builder, this.controlsOptions = const LocationsMapControlsOptions.empty(), this.enableOpenMapsTileLayer = false, this.tileProvider, }); + /// + final MapController? controller; + /// The layers containing everything other than the actual tilelayer. final List additionalLayers; @@ -67,6 +75,9 @@ class LocationsMapOptions { /// final LocationMapMarkerBuilder markerBuilder; + /// + final LocationMapMarkerClusterBuilder markerClusterBuilder; + /// final LocationsMapControlsOptions controlsOptions; @@ -75,9 +86,41 @@ class LocationsMapOptions { final bool enableOpenMapsTileLayer; /// [TileProvider] used of the openmaps tile layer. This can be used to add - /// CancellableNetworkTileProvider or other tile providers to improve + /// NetworkTileProvider or other tile providers to improve /// performance without creating a dependency in flutter_locations. final TileProvider? tileProvider; + + LocationsMapOptions copyWith({ + MapController? controller, + List? additionalLayers, + Location? initialLocation, + VoidCallback? onMapReady, + double? zoom, + double? maxZoom, + double? minZoom, + LocationMapSearchBuilder? searchBuilder, + LocationMapMarkerBuilder? markerBuilder, + LocationMapMarkerClusterBuilder? markerClusterBuilder, + LocationsMapControlsOptions? controlsOptions, + bool? enableOpenMapsTileLayer, + TileProvider? tileProvider, + }) => + LocationsMapOptions( + controller: controller ?? this.controller, + additionalLayers: additionalLayers ?? this.additionalLayers, + initialLocation: initialLocation ?? this.initialLocation, + onMapReady: onMapReady ?? this.onMapReady, + zoom: zoom ?? this.zoom, + maxZoom: maxZoom ?? this.maxZoom, + minZoom: minZoom ?? this.minZoom, + searchBuilder: searchBuilder ?? this.searchBuilder, + markerBuilder: markerBuilder ?? this.markerBuilder, + markerClusterBuilder: markerClusterBuilder ?? this.markerClusterBuilder, + controlsOptions: controlsOptions ?? this.controlsOptions, + enableOpenMapsTileLayer: + enableOpenMapsTileLayer ?? this.enableOpenMapsTileLayer, + tileProvider: tileProvider ?? this.tileProvider, + ); } /// @@ -92,3 +135,9 @@ typedef LocationMapMarkerBuilder = Widget Function( BuildContext context, LocationItem locationItem, ); + +/// +typedef LocationMapMarkerClusterBuilder = Widget Function( + BuildContext, + List markers, +); diff --git a/packages/flutter_locations/lib/src/ui/widgets/defaults/list/list.dart b/packages/flutter_locations/lib/src/ui/widgets/defaults/list/list.dart index dc4fbef..e3dc4dd 100644 --- a/packages/flutter_locations/lib/src/ui/widgets/defaults/list/list.dart +++ b/packages/flutter_locations/lib/src/ui/widgets/defaults/list/list.dart @@ -1,7 +1,6 @@ import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_locations/flutter_locations.dart"; -import "package:flutter_locations/src/util/scope.dart"; /// A widget that lists the locations. class DefaultLocationsList extends HookWidget { diff --git a/packages/flutter_locations/lib/src/ui/widgets/defaults/map/marker.dart b/packages/flutter_locations/lib/src/ui/widgets/defaults/map/marker.dart index 746c2bd..6d9369c 100644 --- a/packages/flutter_locations/lib/src/ui/widgets/defaults/map/marker.dart +++ b/packages/flutter_locations/lib/src/ui/widgets/defaults/map/marker.dart @@ -18,6 +18,7 @@ class DefaultLocationsMapMarker extends StatelessWidget { LocationItem locationItem, ) => DefaultLocationsMapMarker(locationItem: locationItem); + @override Widget build(BuildContext context) => Center( child: Container( @@ -30,3 +31,36 @@ class DefaultLocationsMapMarker extends StatelessWidget { ), ); } + +/// +class DefaultLocationsMapMarkerCluster extends StatelessWidget { + /// + const DefaultLocationsMapMarkerCluster({ + required this.markers, + super.key, + }); + + /// + final List markers; + + /// + static Widget builder( + BuildContext context, + List markers, + ) => + DefaultLocationsMapMarkerCluster(markers: markers); + + @override + Widget build(BuildContext context) => DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).colorScheme.surfaceTint, + ), + child: Center( + child: Text( + markers.length.toString(), + style: const TextStyle(color: Colors.white), + ), + ), + ); +} diff --git a/packages/flutter_locations/lib/src/ui/widgets/home.dart b/packages/flutter_locations/lib/src/ui/widgets/home.dart index dd96425..d256319 100644 --- a/packages/flutter_locations/lib/src/ui/widgets/home.dart +++ b/packages/flutter_locations/lib/src/ui/widgets/home.dart @@ -5,10 +5,7 @@ import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_locations/flutter_locations.dart"; import "package:flutter_locations/src/ui/widgets/list.dart"; import "package:flutter_locations/src/ui/widgets/map/controls.dart"; -import "package:flutter_locations/src/ui/widgets/map/map.dart"; import "package:flutter_locations/src/ui/widgets/search.dart"; -import "package:flutter_locations/src/util/scope.dart"; -import "package:flutter_map/flutter_map.dart"; T? _nullOn(T Function() callback) { try { @@ -22,13 +19,9 @@ T? _nullOn(T Function() callback) { class LocationsHome extends HookWidget { /// [LocationsHome] constructor const LocationsHome({ - this.controller, super.key, }); - /// A passable [MapController] for external controlling. - final MapController? controller; - @override Widget build(BuildContext context) { var repository = LocationsScope.of(context).options.respositoryInterface; @@ -38,7 +31,7 @@ class LocationsHome extends HookWidget { var bounds = useState(null); var query = useState(""); - var mapController = useState(controller ?? MapController()); + var mapController = useState(mapOptions.controller ?? MapController()); void setBounds(LatLngBounds newBounds) => bounds.value = newBounds; void setQuery(String value) => query.value = value; @@ -106,3 +99,58 @@ class LocationsHome extends HookWidget { ); } } + +/// A map that can display locations. +class LocationsMapSimple extends HookWidget { + /// [LocationsMapSimple] constructor + const LocationsMapSimple({ + super.key, + }); + + @override + Widget build(BuildContext context) { + var repository = LocationsScope.of(context).options.respositoryInterface; + var mapOptions = LocationsScope.of(context).options.mapOptions; + + var bounds = useState(null); + + var mapController = useState(mapOptions.controller ?? MapController()); + + void setBounds(LatLngBounds newBounds) => bounds.value = newBounds; + + // We have to try catch on a plain Exception because + // This is what the camera.zoom will throw if + // you fetch it before the first render cycle :( + var zoom = _nullOn( + () => mapController.value.camera.zoom, + ); + + var locationStream = repository.getLocations( + filter: LocationsFilter( + bounds: bounds.value != null + ? LocationBounds( + northWest: Location( + latitude: bounds.value!.north, + longitude: bounds.value!.west, + ), + southEast: Location( + latitude: bounds.value!.south, + longitude: bounds.value!.east, + ), + ) + : null, + zoom: zoom, + ), + ); + + return Stack( + children: [ + LocationsMap( + locationStream: locationStream, + controller: mapController.value, + onSetBounds: setBounds, + ), + ], + ); + } +} diff --git a/packages/flutter_locations/lib/src/ui/widgets/list.dart b/packages/flutter_locations/lib/src/ui/widgets/list.dart index 7b00296..f940e2a 100644 --- a/packages/flutter_locations/lib/src/ui/widgets/list.dart +++ b/packages/flutter_locations/lib/src/ui/widgets/list.dart @@ -1,6 +1,5 @@ import "package:flutter/material.dart"; import "package:flutter_locations/flutter_locations.dart"; -import "package:flutter_locations/src/util/scope.dart"; /// Widget holding the builder. class LocationsList extends StatelessWidget { diff --git a/packages/flutter_locations/lib/src/ui/widgets/map/location_marker.dart b/packages/flutter_locations/lib/src/ui/widgets/map/location_marker.dart index 27db656..fcc9d43 100644 --- a/packages/flutter_locations/lib/src/ui/widgets/map/location_marker.dart +++ b/packages/flutter_locations/lib/src/ui/widgets/map/location_marker.dart @@ -26,14 +26,17 @@ class CurrentLocationMarker extends HookWidget { return const SizedBox.shrink(); } - return CurrentLocationLayer( - // force the widget to rebuild when the gps follow state changes - key: ValueKey(isGpsFollowActive.data), - alignPositionOnUpdate: isGpsFollowActive.data ?? false - ? AlignOnUpdate.always - : AlignOnUpdate.never, - alignDirectionOnUpdate: AlignOnUpdate.never, - headingStream: const Stream.empty(), + return IgnorePointer( + ignoring: true, + child: CurrentLocationLayer( + // force the widget to rebuild when the gps follow state changes + key: ValueKey(isGpsFollowActive.data), + alignPositionOnUpdate: isGpsFollowActive.data ?? false + ? AlignOnUpdate.always + : AlignOnUpdate.never, + alignDirectionOnUpdate: AlignOnUpdate.never, + headingStream: const Stream.empty(), + ), ); } } diff --git a/packages/flutter_locations/lib/src/ui/widgets/map/map.dart b/packages/flutter_locations/lib/src/ui/widgets/map/map.dart index e4754df..1b07d00 100644 --- a/packages/flutter_locations/lib/src/ui/widgets/map/map.dart +++ b/packages/flutter_locations/lib/src/ui/widgets/map/map.dart @@ -4,8 +4,6 @@ import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_locations/flutter_locations.dart"; import "package:flutter_locations/src/ui/widgets/map/location_marker.dart"; -import "package:flutter_locations/src/util/scope.dart"; -import "package:flutter_map/flutter_map.dart"; import "package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart"; import "package:latlong2/latlong.dart"; import "package:platform_maps_flutter/platform_maps_flutter.dart" @@ -87,22 +85,16 @@ class LocationsMap extends HookWidget { alignment: Alignment.center, padding: const EdgeInsets.all(50), maxZoom: 15, - builder: (context, markers) => DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: Theme.of(context).colorScheme.surfaceTint, - ), - child: Center( - child: Text( - markers.length.toString(), - style: const TextStyle(color: Colors.white), - ), - ), - ), + builder: options.markerClusterBuilder, ), ), const CurrentLocationMarker(), - ...options.additionalLayers, + ...options.additionalLayers.map( + (widget) => IgnorePointer( + ignoring: true, + child: widget, + ), + ), ]; // ignore: avoid_positional_boolean_parameters diff --git a/packages/flutter_locations/pubspec.yaml b/packages/flutter_locations/pubspec.yaml index b73bf18..60dd1b5 100644 --- a/packages/flutter_locations/pubspec.yaml +++ b/packages/flutter_locations/pubspec.yaml @@ -6,8 +6,8 @@ homepage: https://github.com/Iconica-Development publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub environment: - sdk: ">=3.4.3 <4.0.0" - flutter: ">=1.17.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.5.0" dependencies: flutter: @@ -16,9 +16,9 @@ dependencies: hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub version: ^1.0.0 flutter_hooks: ^0.18.4 - flutter_map: ^7.0.2 - flutter_map_marker_cluster: ^1.4.0 - flutter_map_location_marker: ^9.1.1 + flutter_map: ^8.2.0 + flutter_map_marker_cluster: ^8.2.2 + flutter_map_location_marker: ^10.2.0 platform_maps_flutter: ^1.0.2 latlong2: ^0.9.1