Add/Edit location data feature (#1593)

This commit is contained in:
Ashil 2023-12-18 21:43:09 +05:30 committed by GitHub
commit 195f84bad0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 645 additions and 34 deletions

View file

@ -8158,6 +8158,56 @@ class S {
);
}
/// `Edit location`
String get editLocation {
return Intl.message(
'Edit location',
name: 'editLocation',
desc: '',
args: [],
);
}
/// `Select a location`
String get selectALocation {
return Intl.message(
'Select a location',
name: 'selectALocation',
desc: '',
args: [],
);
}
/// `Select a location first`
String get selectALocationFirst {
return Intl.message(
'Select a location first',
name: 'selectALocationFirst',
desc: '',
args: [],
);
}
/// `Change location of selected items?`
String get changeLocationOfSelectedItems {
return Intl.message(
'Change location of selected items?',
name: 'changeLocationOfSelectedItems',
desc: '',
args: [],
);
}
/// `Edits to location will only be seen within Ente`
String get editsToLocationWillOnlyBeSeenWithinEnte {
return Intl.message(
'Edits to location will only be seen within Ente',
name: 'editsToLocationWillOnlyBeSeenWithinEnte',
desc: '',
args: [],
);
}
/// `Clean Uncategorized`
String get cleanUncategorized {
return Intl.message(

View file

@ -5,5 +5,10 @@
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
"yourMap": "Your map",
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for",
"contacts": "Contacts"
"contacts": "Contacts",
"editLocation": "Edit location",
"selectALocation": "Select a location",
"selectALocationFirst": "Select a location first",
"changeLocationOfSelectedItems": "Change location of selected items?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
}

View file

@ -1158,5 +1158,10 @@
"signOutFromOtherDevices": "Von anderen Geräten abmelden",
"signOutOtherBody": "Falls du denkst, dass jemand dein Passwort kennen könnte, kannst du alle anderen Geräte von deinem Account abmelden.",
"signOutOtherDevices": "Andere Geräte abmelden",
"doNotSignOut": "Melde dich nicht ab"
"doNotSignOut": "Melde dich nicht ab",
"editLocation": "Edit location",
"selectALocation": "Select a location",
"selectALocationFirst": "Select a location first",
"changeLocationOfSelectedItems": "Change location of selected items?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
}

View file

@ -944,8 +944,8 @@
"itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "It looks like something went wrong. Please retry after some time. If the error persists, please contact our support team.",
"error": "Error",
"tempErrorContactSupportIfPersists": "It looks like something went wrong. Please retry after some time. If the error persists, please contact our support team.",
"networkHostLookUpErr" : "Unable to connect to Ente, please check your network settings and contact support if the error persists.",
"networkConnectionRefusedErr" : "Unable to connect to Ente, please retry after sometime. If the error persists, please contact support.",
"networkHostLookUpErr": "Unable to connect to Ente, please check your network settings and contact support if the error persists.",
"networkConnectionRefusedErr": "Unable to connect to Ente, please retry after sometime. If the error persists, please contact support.",
"cachedData": "Cached data",
"clearCaches": "Clear caches",
"remoteImages": "Remote images",
@ -1170,10 +1170,14 @@
"contacts": "Contacts",
"noInternetConnection": "No internet connection",
"pleaseCheckYourInternetConnectionAndTryAgain": "Please check your internet connection and try again.",
"signOutFromOtherDevices": "Sign out from other devices",
"signOutOtherBody": "If you think someone might know your password, you can force all other devices using your account to sign out.",
"signOutOtherDevices": "Sign out other devices",
"doNotSignOut": "Do not sign out",
"editLocation": "Edit location",
"selectALocation": "Select a location",
"selectALocationFirst": "Select a location first",
"changeLocationOfSelectedItems": "Change location of selected items?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
"cleanUncategorized": "Clean Uncategorized"
}

View file

@ -968,5 +968,10 @@
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
"yourMap": "Your map",
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for",
"contacts": "Contacts"
"contacts": "Contacts",
"editLocation": "Edit location",
"selectALocation": "Select a location",
"selectALocationFirst": "Select a location first",
"changeLocationOfSelectedItems": "Change location of selected items?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
}

View file

@ -1149,5 +1149,10 @@
"@addNew": {
"description": "Text to add a new item (location tag, album, caption etc)"
},
"contacts": "Contacts"
"contacts": "Contacts",
"editLocation": "Edit location",
"selectALocation": "Select a location",
"selectALocationFirst": "Select a location first",
"changeLocationOfSelectedItems": "Change location of selected items?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
}

View file

@ -1111,5 +1111,10 @@
"addOnPageSubtitle": "Dettagli dei componenti aggiuntivi",
"yourMap": "Your map",
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for",
"contacts": "Contacts"
"contacts": "Contacts",
"editLocation": "Edit location",
"selectALocation": "Select a location",
"selectALocationFirst": "Select a location first",
"changeLocationOfSelectedItems": "Change location of selected items?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
}

View file

@ -5,5 +5,10 @@
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
"yourMap": "Your map",
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for",
"contacts": "Contacts"
"contacts": "Contacts",
"editLocation": "Edit location",
"selectALocation": "Select a location",
"selectALocationFirst": "Select a location first",
"changeLocationOfSelectedItems": "Change location of selected items?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
}

View file

@ -1158,5 +1158,10 @@
"signOutFromOtherDevices": "Log uit op andere apparaten",
"signOutOtherBody": "Als je denkt dat iemand je wachtwoord zou kunnen kennen, kun je alle andere apparaten die je account gebruiken dwingen om uit te loggen.",
"signOutOtherDevices": "Log uit op andere apparaten",
"doNotSignOut": "Niet uitloggen"
"doNotSignOut": "Niet uitloggen",
"editLocation": "Edit location",
"selectALocation": "Select a location",
"selectALocationFirst": "Select a location first",
"changeLocationOfSelectedItems": "Change location of selected items?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
}

View file

@ -19,5 +19,10 @@
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
"yourMap": "Your map",
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for",
"contacts": "Contacts"
"contacts": "Contacts",
"editLocation": "Edit location",
"selectALocation": "Select a location",
"selectALocationFirst": "Select a location first",
"changeLocationOfSelectedItems": "Change location of selected items?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
}

View file

@ -106,5 +106,10 @@
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
"yourMap": "Your map",
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for",
"contacts": "Contacts"
"contacts": "Contacts",
"editLocation": "Edit location",
"selectALocation": "Select a location",
"selectALocationFirst": "Select a location first",
"changeLocationOfSelectedItems": "Change location of selected items?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
}

View file

@ -272,5 +272,10 @@
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
"yourMap": "Your map",
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for",
"contacts": "Contacts"
"contacts": "Contacts",
"editLocation": "Edit location",
"selectALocation": "Select a location",
"selectALocationFirst": "Select a location first",
"changeLocationOfSelectedItems": "Change location of selected items?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
}

View file

@ -1173,5 +1173,10 @@
"signOutOtherBody": "如果你认为有人可能知道你的密码,你可以强制所有使用你账户的其他设备退出登录。",
"signOutOtherDevices": "登出其他设备",
"doNotSignOut": "不要退登",
"editLocation": "Edit location",
"selectALocation": "Select a location",
"selectALocationFirst": "Select a location first",
"changeLocationOfSelectedItems": "Change location of selected items?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
"cleanUncategorized": "清除未分类的"
}

View file

@ -14,6 +14,7 @@ import 'package:photos/utils/date_time_util.dart';
import 'package:photos/utils/exif_util.dart';
import 'package:photos/utils/file_uploader_util.dart';
//Todo: files with no location data have lat and long set to 0.0. This should ideally be null.
class EnteFile {
int? generatedID;
int? uploadedFileID;
@ -271,6 +272,7 @@ class EnteFile {
int get width {
return pubMagicMetadata?.w ?? 0;
}
bool get hasDimensions {
return height != 0 && width != 0;
}

View file

@ -242,6 +242,10 @@ extension GalleyTypeExtension on GalleryType {
bool showRemoveFromHiddenAlbum() {
return this == GalleryType.hiddenOwnedCollection;
}
bool showEditLocation() {
return this != GalleryType.sharedCollection;
}
}
extension GalleryAppBarExtn on GalleryType {

View file

@ -1,15 +1,21 @@
import 'package:dio/dio.dart';
import "package:flutter/material.dart";
import "package:latlong2/latlong.dart";
import 'package:logging/logging.dart';
import 'package:path/path.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/network/network.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/extensions/list.dart';
import "package:photos/generated/l10n.dart";
import 'package:photos/models/file/file.dart';
import "package:photos/models/file_load_result.dart";
import "package:photos/models/metadata/file_magic.dart";
import 'package:photos/services/file_magic_service.dart';
import "package:photos/services/ignored_files_service.dart";
import "package:photos/ui/components/action_sheet_widget.dart";
import "package:photos/ui/components/buttons/button_widget.dart";
import "package:photos/ui/components/models/button_type.dart";
import 'package:photos/utils/date_time_util.dart';
class FilesService {
@ -85,6 +91,79 @@ class FilesService {
}
}
Future<void> bulkEditLocationData(
List<EnteFile> files,
LatLng location,
BuildContext context,
) async {
final List<EnteFile> uploadedFiles =
files.where((element) => element.uploadedFileID != null).toList();
final List<EnteFile> remoteFilesToUpdate = [];
final Map<int, Map<String, dynamic>> fileIDToUpdateMetadata = {};
await showActionSheet(
context: context,
body: S.of(context).changeLocationOfSelectedItems,
buttons: [
ButtonWidget(
labelText: S.of(context).yes,
buttonType: ButtonType.neutral,
buttonSize: ButtonSize.large,
shouldStickToDarkTheme: true,
buttonAction: ButtonAction.first,
shouldSurfaceExecutionStates: true,
isInAlert: true,
onTap: () async {
await _editLocationData(
uploadedFiles,
fileIDToUpdateMetadata,
remoteFilesToUpdate,
location,
);
},
),
ButtonWidget(
labelText: S.of(context).cancel,
buttonType: ButtonType.secondary,
buttonSize: ButtonSize.large,
shouldStickToDarkTheme: true,
buttonAction: ButtonAction.cancel,
isInAlert: true,
),
],
);
}
Future<void> _editLocationData(
List<EnteFile> uploadedFiles,
Map<int, Map<String, dynamic>> fileIDToUpdateMetadata,
List<EnteFile> remoteFilesToUpdate,
LatLng location,
) async {
for (EnteFile remoteFile in uploadedFiles) {
// discard files not owned by user and also dedupe already processed
// files
if (remoteFile.ownerID != _config.getUserID()! ||
fileIDToUpdateMetadata.containsKey(remoteFile.uploadedFileID!)) {
continue;
}
remoteFilesToUpdate.add(remoteFile);
fileIDToUpdateMetadata[remoteFile.uploadedFileID!] = {
latKey: location.latitude,
longKey: location.longitude,
};
}
if (remoteFilesToUpdate.isNotEmpty) {
await FileMagicService.instance.updatePublicMagicMetadata(
remoteFilesToUpdate,
null,
metadataUpdateMap: fileIDToUpdateMetadata,
);
}
}
// Note: this method is not used anywhere, but it is kept for future
// reference when we add bulk EditTime feature
Future<void> bulkEditTime(

View file

@ -114,17 +114,25 @@ class LocationService {
return false;
}
String convertLocationToDMS(Location centerPoint) {
/// returns [lat, lng]
List<String>? convertLocationToDMS(Location centerPoint) {
if (centerPoint.latitude == null || centerPoint.longitude == null) {
return null;
}
final lat = centerPoint.latitude!;
final long = centerPoint.longitude!;
final latRef = lat >= 0 ? "N" : "S";
final longRef = long >= 0 ? "E" : "W";
final latDMS = convertCoordinateToDMS(lat.abs());
final longDMS = convertCoordinateToDMS(long.abs());
return "${latDMS[0]}°${latDMS[1]}'${latDMS[2]}\"$latRef, ${longDMS[0]}°${longDMS[1]}'${longDMS[2]}\"$longRef";
final latDMS = _convertCoordinateToDMS(lat.abs());
final longDMS = _convertCoordinateToDMS(long.abs());
return [
"${latDMS[0]}°${latDMS[1]}'${latDMS[2]}\" $latRef",
"${longDMS[0]}°${longDMS[1]}'${longDMS[2]}\" $longRef",
];
}
List<int> convertCoordinateToDMS(double coordinate) {
List<int> _convertCoordinateToDMS(double coordinate) {
final degrees = coordinate.floor();
final minutes = ((coordinate - degrees) * 60).floor();
final seconds = ((coordinate - degrees - minutes / 60) * 3600).floor();

View file

@ -45,7 +45,7 @@ class _MapScreenState extends State<MapScreen> {
double maxZoom = 18.0;
double minZoom = 2.8;
int debounceDuration = 500;
LatLng center = LatLng(46.7286, 4.8614);
LatLng center = const LatLng(46.7286, 4.8614);
final Logger _logger = Logger("_MapScreenState");
StreamSubscription? _mapMoveSubscription;
Isolate? isolate;

View file

@ -77,8 +77,8 @@ class _MapViewState extends State<MapView> {
enableMultiFingerGestureRace: true,
zoom: widget.initialZoom,
maxBounds: LatLngBounds(
LatLng(-90, -180),
LatLng(90, 180),
const LatLng(-90, -180),
const LatLng(90, 180),
),
onPositionChanged: (position, hasGesture) {
if (position.bounds != null) {

View file

@ -3,6 +3,7 @@ import "dart:async";
import 'package:fast_base58/fast_base58.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
import 'package:photos/core/configuration.dart';
import "package:photos/generated/l10n.dart";
import 'package:photos/models/collection/collection.dart';
@ -15,6 +16,8 @@ import "package:photos/models/metadata/common_keys.dart";
import 'package:photos/models/selected_files.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/hidden_service.dart';
import "package:photos/theme/colors.dart";
import "package:photos/theme/ente_theme.dart";
import 'package:photos/ui/actions/collection/collection_file_actions.dart';
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
import 'package:photos/ui/collections/collection_action_sheet.dart';
@ -24,6 +27,7 @@ import 'package:photos/ui/components/buttons/button_widget.dart';
import 'package:photos/ui/components/models/button_type.dart';
import 'package:photos/ui/sharing/manage_links_widget.dart';
import "package:photos/ui/tools/collage/collage_creator_page.dart";
import "package:photos/ui/viewer/location/update_location_data_widget.dart";
import 'package:photos/utils/delete_file_util.dart';
import 'package:photos/utils/magic_util.dart';
import 'package:photos/utils/navigation_util.dart';
@ -308,6 +312,56 @@ class _FileSelectionActionsWidgetState
);
}
if (widget.type.showEditLocation()) {
items.add(
SelectionActionButton(
shouldShow: widget.selectedFiles.files.any(
(element) => (element.ownerID == currentUserID),
),
labelText: S.of(context).editLocation,
icon: Icons.edit_location_alt_outlined,
onTap: () async {
await showBarModalBottomSheet(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(5),
),
),
backgroundColor: getEnteColorScheme(context).backgroundElevated,
barrierColor: backdropFaintDark,
topControl: Stack(
alignment: Alignment.bottomCenter,
children: [
// This container is for increasing the tap area
Container(
width: double.infinity,
height: 36,
color: Colors.transparent,
),
Container(
height: 5,
width: 40,
decoration: const BoxDecoration(
color: backgroundElevated2Light,
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
),
],
),
context: context,
builder: (context) {
return UpdateLocationDataWidget(
widget.selectedFiles.files.toList(),
);
},
);
},
),
);
}
items.add(
SelectionActionButton(
labelText: S.of(context).share,

View file

@ -86,6 +86,7 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
getExif(widget.file).then((exif) {
_exifNotifier.value = exif;
});
super.initState();
}
@ -152,12 +153,52 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
? Column(
children: [
LocationTagsWidget(
widget.file.location!,
widget.file,
),
const FileDetailsDivider(),
],
)
: const SizedBox.shrink();
///To be used when state issues are fixed when location is updated.
//
// file.fileType != FileType.video &&
// file.ownerID == _currentUserID
// ? Column(
// children: [
// InfoItemWidget(
// leadingIcon: Icons.pin_drop_outlined,
// title: "No location data",
// subtitleSection: Future.value(
// [
// Text(
// "Add location data",
// style: getEnteTextTheme(context).miniBoldMuted,
// ),
// ],
// ),
// hasChipButtons: false,
// onTap: () async {
// await showBarModalBottomSheet(
// shape: const RoundedRectangleBorder(
// borderRadius: BorderRadius.vertical(
// top: Radius.circular(5),
// ),
// ),
// backgroundColor: getEnteColorScheme(context)
// .backgroundElevated,
// barrierColor: backdropFaintDark,
// context: context,
// builder: (context) {
// return UpdateLocationDataWidget([file]);
// },
// );
// },
// ),
// const FileDetailsDivider(),
// ],
// )
// : const SizedBox.shrink();
},
),
]);
@ -280,7 +321,8 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
if (imageWidth != null && imageLength != null) {
_exifData["resolution"] = '$imageWidth x $imageLength';
final double megaPixels =
(imageWidth.values.firstAsInt() * imageLength.values.firstAsInt()) / 1000000;
(imageWidth.values.firstAsInt() * imageLength.values.firstAsInt()) /
1000000;
final double roundedMegaPixels = (megaPixels * 10).round() / 10.0;
_exifData['megaPixels'] = roundedMegaPixels..toStringAsFixed(1);
} else {

View file

@ -4,7 +4,7 @@ import "package:flutter/material.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/events/location_tag_updated_event.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/models/location/location.dart";
import "package:photos/models/file/file.dart";
import "package:photos/services/location_service.dart";
import "package:photos/states/location_screen_state.dart";
import "package:photos/theme/ente_theme.dart";
@ -15,8 +15,8 @@ import "package:photos/ui/viewer/location/location_screen.dart";
import "package:photos/utils/navigation_util.dart";
class LocationTagsWidget extends StatefulWidget {
final Location centerPoint;
const LocationTagsWidget(this.centerPoint, {super.key});
final EnteFile file;
const LocationTagsWidget(this.file, {super.key});
@override
State<LocationTagsWidget> createState() => _LocationTagsWidgetState();
@ -58,13 +58,33 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
subtitleSection: locationTagChips,
hasChipButtons: hasChipButtons ?? true,
onTap: onTap,
/// to be used when state issues are fixed when location is updated
// editOnTap: widget.file.ownerID == Configuration.instance.getUserID()!
// ? () {
// showBarModalBottomSheet(
// shape: const RoundedRectangleBorder(
// borderRadius: BorderRadius.vertical(
// top: Radius.circular(5),
// ),
// ),
// backgroundColor:
// getEnteColorScheme(context).backgroundElevated,
// barrierColor: backdropFaintDark,
// context: context,
// builder: (context) {
// return UpdateLocationDataWidget([widget.file]);
// },
// );
// }
// : null,
),
);
}
Future<List<Widget>> _getLocationTags() async {
final locationTags = await LocationService.instance
.enclosingLocationTags(widget.centerPoint);
.enclosingLocationTags(widget.file.location!);
if (locationTags.isEmpty) {
if (mounted) {
setState(() {
@ -73,7 +93,7 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
hasChipButtons = false;
onTap = () => showAddLocationSheet(
context,
widget.centerPoint,
widget.file.location!,
);
});
}
@ -112,7 +132,7 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
ChipButtonWidget(
null,
leadingIcon: Icons.add_outlined,
onTap: () => showAddLocationSheet(context, widget.centerPoint),
onTap: () => showAddLocationSheet(context, widget.file.location!),
),
);
return result;

View file

@ -14,6 +14,9 @@ class EditCenterPointTileWidget extends StatelessWidget {
Widget build(BuildContext context) {
final textTheme = getEnteTextTheme(context);
final colorScheme = getEnteColorScheme(context);
final centerPointInDMS = LocationService.instance.convertLocationToDMS(
InheritedLocationTagData.of(context).centerPoint,
);
return Row(
children: [
Container(
@ -39,9 +42,7 @@ class EditCenterPointTileWidget extends StatelessWidget {
),
const SizedBox(height: 4),
Text(
LocationService.instance.convertLocationToDMS(
InheritedLocationTagData.of(context).centerPoint,
),
"${centerPointInDMS![0]}, ${centerPointInDMS[1]}",
style: textTheme.miniMuted,
),
],

View file

@ -0,0 +1,290 @@
import "dart:async";
import "package:flutter/material.dart";
import "package:flutter_map/flutter_map.dart";
import "package:latlong2/latlong.dart";
import "package:logging/logging.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/models/file/file.dart";
import "package:photos/models/location/location.dart";
import "package:photos/services/files_service.dart";
import "package:photos/services/location_service.dart";
import "package:photos/theme/effects.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/map/map_button.dart";
import "package:photos/ui/map/tile/layers.dart";
import "package:photos/utils/toast_util.dart";
class UpdateLocationDataWidget extends StatefulWidget {
final List<EnteFile> files;
const UpdateLocationDataWidget(this.files, {super.key});
@override
State<UpdateLocationDataWidget> createState() =>
_UpdateLocationDataWidgetState();
}
class _UpdateLocationDataWidgetState extends State<UpdateLocationDataWidget> {
final MapController _mapController = MapController();
ValueNotifier hasSelectedLocation = ValueNotifier(false);
final selectedLocation = ValueNotifier<LatLng?>(null);
final isDragging = ValueNotifier(false);
@override
void dispose() {
super.dispose();
hasSelectedLocation.dispose();
selectedLocation.dispose();
_mapController.dispose();
isDragging.dispose();
}
@override
Widget build(BuildContext context) {
Logger("UpdateLocationDataWiget").info("building");
final textTheme = getEnteTextTheme(context);
return Stack(
alignment: Alignment.topCenter,
children: [
FlutterMap(
mapController: _mapController,
options: MapOptions(
enableMultiFingerGestureRace: true,
zoom: 3,
maxZoom: 18.0,
minZoom: 2.8,
onMapEvent: (p0) {
if (p0.source == MapEventSource.onDrag) {
isDragging.value = true;
} else if (p0.source == MapEventSource.dragEnd) {
isDragging.value = false;
}
},
onTap: (tapPosition, latlng) {
final zoom = selectedLocation.value == null
? _mapController.zoom + 2.0
: _mapController.zoom;
_mapController.move(latlng, zoom);
selectedLocation.value = latlng;
hasSelectedLocation.value = true;
},
onPositionChanged: (position, hasGesture) {
if (selectedLocation.value != null) {
selectedLocation.value = position.center;
}
},
),
nonRotatedChildren: const [
OSMFranceTileAttributes(),
],
children: const [
OSMFranceTileLayer(),
],
),
Positioned(
top: 20,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: getEnteColorScheme(context).backgroundElevated,
boxShadow: shadowFloatFaintLight,
),
child: ValueListenableBuilder(
valueListenable: selectedLocation,
builder: (context, value, _) {
final locationInDMS =
LocationService.instance.convertLocationToDMS(
Location(
latitude: value?.latitude,
longitude: value?.longitude,
),
);
return locationInDMS != null
? ConstrainedBox(
constraints: BoxConstraints(
minWidth: 80 * MediaQuery.textScaleFactorOf(context),
),
child: Column(
children: [
Text(
locationInDMS[0],
style: textTheme.mini,
),
const SizedBox(height: 8),
Text(
locationInDMS[1],
style: textTheme.mini,
),
],
),
)
: const UpdateLocationInfo();
},
),
),
),
Positioned(
bottom: 48,
right: 24,
left: 24,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
MapButton(
icon: Icons.add,
onPressed: () {
_mapController.move(
_mapController.center,
_mapController.zoom + 1,
);
},
heroTag: 'zoom-in',
),
const SizedBox(height: 8),
MapButton(
icon: Icons.remove,
onPressed: () {
_mapController.move(
_mapController.center,
_mapController.zoom - 1,
);
},
heroTag: 'zoom-out',
),
const SizedBox(height: 8),
MapButton(
icon: Icons.check,
onPressed: () async {
if (selectedLocation.value == null) {
unawaited(
showShortToast(
context,
S.of(context).selectALocationFirst,
),
);
return;
}
await FilesService.instance.bulkEditLocationData(
widget.files,
selectedLocation.value!,
context,
);
Navigator.of(context).pop();
},
heroTag: 'add-location',
),
],
),
),
ValueListenableBuilder(
valueListenable: hasSelectedLocation,
builder: (context, value, _) {
return value
? Positioned(
bottom: 32,
top: 0,
left: 0,
right: 0,
child: Stack(
alignment: Alignment.center,
children: [
ValueListenableBuilder(
valueListenable: isDragging,
builder: (context, value, child) {
return AnimatedContainer(
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 200),
height: value ? 32 : 16,
child: child,
);
},
child: const Icon(
Icons.location_on,
color: Color.fromARGB(255, 250, 34, 19),
size: 32,
),
),
Transform(
transform: Matrix4.translationValues(0, 21, 0),
child: Container(
height: 2,
width: 12,
decoration: BoxDecoration(
boxShadow: shadowMenuDark,
),
),
),
],
),
)
: const SizedBox.shrink();
},
),
],
);
}
}
class UpdateLocationInfo extends StatefulWidget {
const UpdateLocationInfo({super.key});
@override
State<UpdateLocationInfo> createState() => _UpdateLocationInfoState();
}
class _UpdateLocationInfoState extends State<UpdateLocationInfo> {
bool showSelectLocationText = false;
@override
initState() {
super.initState();
Future.delayed(const Duration(seconds: 3), () {
setState(() {
showSelectLocationText = true;
});
});
}
@override
Widget build(BuildContext context) {
return AnimatedCrossFade(
duration: const Duration(milliseconds: 200),
firstCurve: Curves.easeInOutExpo,
secondCurve: Curves.easeInOutExpo,
sizeCurve: Curves.easeInOutExpo,
crossFadeState: showSelectLocationText
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: Text(
S.of(context).selectALocation,
style: getEnteTextTheme(context).mini,
),
secondChild: Text(
S.of(context).editsToLocationWillOnlyBeSeenWithinEnte,
style: getEnteTextTheme(context).mini,
),
layoutBuilder: (topChild, topChildKey, bottomChild, bottomChildKey) {
return Stack(
alignment: Alignment.center,
children: [
Positioned(
top: 0,
key: bottomChildKey,
child: bottomChild,
// top: 0,
),
Positioned(
key: topChildKey,
child: topChild,
),
],
);
},
);
}
}

View file

@ -1,3 +1,5 @@
import "dart:async";
import "package:flutter/material.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/services/search_service.dart";

View file

@ -66,7 +66,7 @@ Future<DateTime?> getCreationTimeFromEXIF(
Location? locationFromExif(Map<String, IfdTag> exif) {
try {
return _gpsDataFromExif(exif).toLocationObj();
return gpsDataFromExif(exif).toLocationObj();
} catch (e, s) {
_logger.severe("failed to get location from exif", e, s);
return null;
@ -85,7 +85,7 @@ Future<Map<String, IfdTag>> readExifAsync(File file) async {
);
}
GPSData _gpsDataFromExif(Map<String, IfdTag> exif) {
GPSData gpsDataFromExif(Map<String, IfdTag> exif) {
final Map<String, dynamic> exifLocationData = {
"lat": null,
"long": null,