Add/Edit location data feature (#1593)
This commit is contained in:
commit
195f84bad0
26 changed files with 645 additions and 34 deletions
50
lib/generated/l10n.dart
generated
50
lib/generated/l10n.dart
generated
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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": "清除未分类的"
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -242,6 +242,10 @@ extension GalleyTypeExtension on GalleryType {
|
|||
bool showRemoveFromHiddenAlbum() {
|
||||
return this == GalleryType.hiddenOwnedCollection;
|
||||
}
|
||||
|
||||
bool showEditLocation() {
|
||||
return this != GalleryType.sharedCollection;
|
||||
}
|
||||
}
|
||||
|
||||
extension GalleryAppBarExtn on GalleryType {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
|
|
290
lib/ui/viewer/location/update_location_data_widget.dart
Normal file
290
lib/ui/viewer/location/update_location_data_widget.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue