mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
feat(mobile): map improvements (#17714)
* fix: remove unnecessary db operations in map * feat: use user's location for map thumbnails * chore: refactored handleMapEvents * fix: location fails fetching & update geolocator * chore: minor refactor * chore: small style tweak --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
c49fd2065b
commit
f0ff8581da
@ -1,6 +1,7 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
@ -12,6 +13,7 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
import 'package:immich_mobile/utils/map_utils.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/common/user_avatar.dart';
|
||||
@ -297,32 +299,34 @@ class LocalAlbumsCollectionCard extends HookConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
SizedBox(
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withAlpha(30),
|
||||
context.colorScheme.primary.withAlpha(25),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withAlpha(30),
|
||||
context.colorScheme.primary.withAlpha(25),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: GridView.count(
|
||||
crossAxisCount: 2,
|
||||
padding: const EdgeInsets.all(12),
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: albums.take(4).map((album) {
|
||||
return AlbumThumbnailCard(
|
||||
album: album,
|
||||
showTitle: false,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
child: GridView.count(
|
||||
crossAxisCount: 2,
|
||||
padding: const EdgeInsets.all(12),
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: albums.take(4).map((album) {
|
||||
return AlbumThumbnailCard(
|
||||
album: album,
|
||||
showTitle: false,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
@ -353,43 +357,66 @@ class PlacesCollectionCard extends StatelessWidget {
|
||||
final widthFactor = isTablet ? 0.25 : 0.5;
|
||||
final size = context.width * widthFactor - 20.0;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(const PlacesCollectionRoute()),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: context.colorScheme.secondaryContainer.withAlpha(100),
|
||||
),
|
||||
child: IgnorePointer(
|
||||
child: MapThumbnail(
|
||||
zoom: 8,
|
||||
centre: const LatLng(
|
||||
21.44950,
|
||||
-157.91959,
|
||||
),
|
||||
showAttribution: false,
|
||||
themeMode:
|
||||
context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'places'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
return FutureBuilder<(Position?, LocationPermission?)>(
|
||||
future: MapUtils.checkPermAndGetLocation(
|
||||
context: context,
|
||||
silent: true,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
var position = snapshot.data?.$1;
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(
|
||||
PlacesCollectionRoute(
|
||||
currentLocation: position != null
|
||||
? LatLng(position.latitude, position.longitude)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: size,
|
||||
width: size,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(20)),
|
||||
color: context.colorScheme.secondaryContainer
|
||||
.withAlpha(100),
|
||||
),
|
||||
child: IgnorePointer(
|
||||
child: snapshot.connectionState ==
|
||||
ConnectionState.waiting
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: MapThumbnail(
|
||||
zoom: 8,
|
||||
centre: LatLng(
|
||||
position?.latitude ?? 21.44950,
|
||||
position?.longitude ?? -157.91959,
|
||||
),
|
||||
showAttribution: false,
|
||||
themeMode: context.isDarkTheme
|
||||
? ThemeMode.dark
|
||||
: ThemeMode.light,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'places'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -19,7 +19,8 @@ import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
@RoutePage()
|
||||
class PlacesCollectionPage extends HookConsumerWidget {
|
||||
const PlacesCollectionPage({super.key});
|
||||
const PlacesCollectionPage({super.key, this.currentLocation});
|
||||
final LatLng? currentLocation;
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final places = ref.watch(getAllPlacesProvider);
|
||||
@ -58,12 +59,14 @@ class PlacesCollectionPage extends HookConsumerWidget {
|
||||
height: 200,
|
||||
width: context.width,
|
||||
child: MapThumbnail(
|
||||
onTap: (_, __) => context.pushRoute(const MapRoute()),
|
||||
onTap: (_, __) => context
|
||||
.pushRoute(MapRoute(initialLocation: currentLocation)),
|
||||
zoom: 8,
|
||||
centre: const LatLng(
|
||||
21.44950,
|
||||
-157.91959,
|
||||
),
|
||||
centre: currentLocation ??
|
||||
const LatLng(
|
||||
21.44950,
|
||||
-157.91959,
|
||||
),
|
||||
showAttribution: false,
|
||||
themeMode:
|
||||
context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||
|
@ -34,7 +34,8 @@ import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
@RoutePage()
|
||||
class MapPage extends HookConsumerWidget {
|
||||
const MapPage({super.key});
|
||||
const MapPage({super.key, this.initialLocation});
|
||||
final LatLng? initialLocation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@ -235,7 +236,8 @@ class MapPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
void onZoomToLocation() async {
|
||||
final (location, error) = await MapUtils.checkPermAndGetLocation(context);
|
||||
final (location, error) =
|
||||
await MapUtils.checkPermAndGetLocation(context: context);
|
||||
if (error != null) {
|
||||
if (error == LocationPermission.unableToDetermine && context.mounted) {
|
||||
ImmichToast.show(
|
||||
@ -272,6 +274,7 @@ class MapPage extends HookConsumerWidget {
|
||||
body: Stack(
|
||||
children: [
|
||||
_MapWithMarker(
|
||||
initialLocation: initialLocation,
|
||||
style: style,
|
||||
selectedMarker: selectedMarker,
|
||||
onMapCreated: onMapCreated,
|
||||
@ -303,6 +306,7 @@ class MapPage extends HookConsumerWidget {
|
||||
body: Stack(
|
||||
children: [
|
||||
_MapWithMarker(
|
||||
initialLocation: initialLocation,
|
||||
style: style,
|
||||
selectedMarker: selectedMarker,
|
||||
onMapCreated: onMapCreated,
|
||||
@ -368,6 +372,7 @@ class _MapWithMarker extends StatelessWidget {
|
||||
final OnStyleLoadedCallback onStyleLoaded;
|
||||
final Function()? onMarkerTapped;
|
||||
final ValueNotifier<_AssetMarkerMeta?> selectedMarker;
|
||||
final LatLng? initialLocation;
|
||||
|
||||
const _MapWithMarker({
|
||||
required this.style,
|
||||
@ -377,6 +382,7 @@ class _MapWithMarker extends StatelessWidget {
|
||||
required this.onStyleLoaded,
|
||||
required this.selectedMarker,
|
||||
this.onMarkerTapped,
|
||||
this.initialLocation,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -389,8 +395,10 @@ class _MapWithMarker extends StatelessWidget {
|
||||
children: [
|
||||
style.widgetWhen(
|
||||
onData: (style) => MapLibreMap(
|
||||
initialCameraPosition:
|
||||
const CameraPosition(target: LatLng(0, 0)),
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: initialLocation ?? const LatLng(0, 0),
|
||||
zoom: initialLocation != null ? 12 : 0,
|
||||
),
|
||||
styleString: style,
|
||||
// This is needed to update the selectedMarker's position on map camera updates
|
||||
// The changes are notified through the mapController ValueListener which is added in [onMapCreated]
|
||||
|
@ -46,7 +46,7 @@ class MapLocationPickerPage extends HookConsumerWidget {
|
||||
|
||||
Future<void> getCurrentLocation() async {
|
||||
var (currentLocation, _) =
|
||||
await MapUtils.checkPermAndGetLocation(context);
|
||||
await MapUtils.checkPermAndGetLocation(context: context);
|
||||
|
||||
if (currentLocation == null) {
|
||||
return;
|
||||
|
@ -1024,10 +1024,17 @@ class MapLocationPickerRouteArgs {
|
||||
|
||||
/// generated route for
|
||||
/// [MapPage]
|
||||
class MapRoute extends PageRouteInfo<void> {
|
||||
const MapRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
class MapRoute extends PageRouteInfo<MapRouteArgs> {
|
||||
MapRoute({
|
||||
Key? key,
|
||||
LatLng? initialLocation,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
MapRoute.name,
|
||||
args: MapRouteArgs(
|
||||
key: key,
|
||||
initialLocation: initialLocation,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
@ -1036,11 +1043,32 @@ class MapRoute extends PageRouteInfo<void> {
|
||||
static PageInfo page = PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const MapPage();
|
||||
final args =
|
||||
data.argsAs<MapRouteArgs>(orElse: () => const MapRouteArgs());
|
||||
return MapPage(
|
||||
key: args.key,
|
||||
initialLocation: args.initialLocation,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class MapRouteArgs {
|
||||
const MapRouteArgs({
|
||||
this.key,
|
||||
this.initialLocation,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final LatLng? initialLocation;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MapRouteArgs{key: $key, initialLocation: $initialLocation}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [MemoryPage]
|
||||
class MemoryRoute extends PageRouteInfo<MemoryRouteArgs> {
|
||||
@ -1333,10 +1361,17 @@ class PhotosRoute extends PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [PlacesCollectionPage]
|
||||
class PlacesCollectionRoute extends PageRouteInfo<void> {
|
||||
const PlacesCollectionRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
class PlacesCollectionRoute extends PageRouteInfo<PlacesCollectionRouteArgs> {
|
||||
PlacesCollectionRoute({
|
||||
Key? key,
|
||||
LatLng? currentLocation,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
PlacesCollectionRoute.name,
|
||||
args: PlacesCollectionRouteArgs(
|
||||
key: key,
|
||||
currentLocation: currentLocation,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
@ -1345,11 +1380,32 @@ class PlacesCollectionRoute extends PageRouteInfo<void> {
|
||||
static PageInfo page = PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const PlacesCollectionPage();
|
||||
final args = data.argsAs<PlacesCollectionRouteArgs>(
|
||||
orElse: () => const PlacesCollectionRouteArgs());
|
||||
return PlacesCollectionPage(
|
||||
key: args.key,
|
||||
currentLocation: args.currentLocation,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class PlacesCollectionRouteArgs {
|
||||
const PlacesCollectionRouteArgs({
|
||||
this.key,
|
||||
this.currentLocation,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final LatLng? currentLocation;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PlacesCollectionRouteArgs{key: $key, currentLocation: $currentLocation}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [RecentlyAddedPage]
|
||||
class RecentlyAddedRoute extends PageRouteInfo<void> {
|
||||
|
@ -64,12 +64,13 @@ class MapUtils {
|
||||
'features': markers.map(_addFeature).toList(),
|
||||
};
|
||||
|
||||
static Future<(Position?, LocationPermission?)> checkPermAndGetLocation(
|
||||
BuildContext context,
|
||||
) async {
|
||||
static Future<(Position?, LocationPermission?)> checkPermAndGetLocation({
|
||||
required BuildContext context,
|
||||
bool silent = false,
|
||||
}) async {
|
||||
try {
|
||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
if (!serviceEnabled && !silent) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => _LocationServiceDisabledDialog(),
|
||||
@ -80,7 +81,7 @@ class MapUtils {
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
bool shouldRequestPermission = false;
|
||||
|
||||
if (permission == LocationPermission.denied) {
|
||||
if (permission == LocationPermission.denied && !silent) {
|
||||
shouldRequestPermission = await showDialog(
|
||||
context: context,
|
||||
builder: (context) => _LocationPermissionDisabledDialog(),
|
||||
@ -94,15 +95,19 @@ class MapUtils {
|
||||
permission == LocationPermission.deniedForever) {
|
||||
// Open app settings only if you did not request for permission before
|
||||
if (permission == LocationPermission.deniedForever &&
|
||||
!shouldRequestPermission) {
|
||||
!shouldRequestPermission &&
|
||||
!silent) {
|
||||
await Geolocator.openAppSettings();
|
||||
}
|
||||
return (null, LocationPermission.deniedForever);
|
||||
}
|
||||
|
||||
Position currentUserLocation = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.medium,
|
||||
timeLimit: const Duration(seconds: 5),
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
distanceFilter: 0,
|
||||
timeLimit: Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
return (currentUserLocation, null);
|
||||
} catch (error, stack) {
|
||||
|
@ -46,12 +46,39 @@ class MapAssetGrid extends HookConsumerWidget {
|
||||
final gridScrollThrottler =
|
||||
useThrottler(interval: const Duration(milliseconds: 300));
|
||||
|
||||
// Add a cache for assets we've already loaded
|
||||
final assetCache = useRef<Map<String, Asset>>({});
|
||||
|
||||
void handleMapEvents(MapEvent event) async {
|
||||
if (event is MapAssetsInBoundsUpdated) {
|
||||
assetsInBounds.value = await ref
|
||||
.read(dbProvider)
|
||||
.assets
|
||||
.getAllByRemoteId(event.assetRemoteIds);
|
||||
final assetIds = event.assetRemoteIds;
|
||||
final missingIds = <String>[];
|
||||
final currentAssets = <Asset>[];
|
||||
|
||||
for (final id in assetIds) {
|
||||
final asset = assetCache.value[id];
|
||||
if (asset != null) {
|
||||
currentAssets.add(asset);
|
||||
} else {
|
||||
missingIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Only fetch missing assets
|
||||
if (missingIds.isNotEmpty) {
|
||||
final newAssets =
|
||||
await ref.read(dbProvider).assets.getAllByRemoteId(missingIds);
|
||||
|
||||
// Add new assets to cache and current list
|
||||
for (final asset in newAssets) {
|
||||
if (asset.remoteId != null) {
|
||||
assetCache.value[asset.remoteId!] = asset;
|
||||
currentAssets.add(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assetsInBounds.value = currentAssets;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -124,7 +151,7 @@ class MapAssetGrid extends HookConsumerWidget {
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: FractionallySizedBox(
|
||||
// Place it just below the drag handle
|
||||
heightFactor: 0.80,
|
||||
heightFactor: 0.87,
|
||||
child: assetsInBounds.value.isNotEmpty
|
||||
? ref
|
||||
.watch(assetsTimelineProvider(assetsInBounds.value))
|
||||
@ -251,8 +278,18 @@ class _MapSheetDragRegion extends StatelessWidget {
|
||||
const SizedBox(height: 15),
|
||||
const CustomDraggingHandle(),
|
||||
const SizedBox(height: 15),
|
||||
Text(assetsInBoundsText, style: context.textTheme.bodyLarge),
|
||||
const Divider(height: 35),
|
||||
Center(
|
||||
child: Text(
|
||||
assetsInBoundsText,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: context.textTheme.displayLarge?.color
|
||||
?.withValues(alpha: 0.75),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
@ -260,14 +297,14 @@ class _MapSheetDragRegion extends StatelessWidget {
|
||||
builder: (_, value, __) => Visibility(
|
||||
visible: value != null,
|
||||
child: Positioned(
|
||||
right: 15,
|
||||
top: 15,
|
||||
right: 18,
|
||||
top: 24,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.map_outlined,
|
||||
color: context.textTheme.displayLarge?.color,
|
||||
),
|
||||
iconSize: 20,
|
||||
iconSize: 24,
|
||||
tooltip: 'Zoom to bounds',
|
||||
onPressed: () => onZoomToAsset?.call(value!),
|
||||
),
|
||||
|
@ -20,7 +20,7 @@ class SearchMapThumbnail extends StatelessWidget {
|
||||
return ThumbnailWithInfoContainer(
|
||||
label: 'search_page_your_map'.tr(),
|
||||
onTap: () {
|
||||
context.pushRoute(const MapRoute());
|
||||
context.pushRoute(MapRoute());
|
||||
},
|
||||
child: IgnorePointer(
|
||||
child: MapThumbnail(
|
||||
|
@ -696,18 +696,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: geolocator
|
||||
sha256: "6cb9fb6e5928b58b9a84bdf85012d757fd07aab8215c5205337021c4999bad27"
|
||||
sha256: e7ebfa04ce451daf39b5499108c973189a71a919aa53c1204effda1c5b93b822
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.1.0"
|
||||
version: "14.0.0"
|
||||
geolocator_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_android
|
||||
sha256: "7aefc530db47d90d0580b552df3242440a10fe60814496a979aa67aa98b1fd47"
|
||||
sha256: "114072db5d1dce0ec0b36af2697f55c133bc89a2c8dd513e137c0afe59696ed4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.1"
|
||||
version: "5.0.1+1"
|
||||
geolocator_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -728,10 +728,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_web
|
||||
sha256: "49d8f846ebeb5e2b6641fe477a7e97e5dd73f03cbfef3fd5c42177b7300fb0ed"
|
||||
sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "4.1.3"
|
||||
geolocator_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -35,7 +35,7 @@ dependencies:
|
||||
flutter_udid: ^3.0.0
|
||||
flutter_web_auth_2: ^5.0.0-alpha.0
|
||||
fluttertoast: ^8.2.12
|
||||
geolocator: ^11.0.0
|
||||
geolocator: ^14.0.0
|
||||
hooks_riverpod: ^2.6.1
|
||||
http: ^1.3.0
|
||||
image_picker: ^1.1.2
|
||||
|
Loading…
x
Reference in New Issue
Block a user