diff --git a/.vscode/launch.json.example b/.vscode/launch.json.example index 974de11cb..1e6b9ae6f 100644 --- a/.vscode/launch.json.example +++ b/.vscode/launch.json.example @@ -26,6 +26,8 @@ "independent", "--dart-define", "endpoint=http://localhost:8080", + "--dart-define", + "web-family=http://localhost:3003", ] }, { @@ -41,10 +43,12 @@ "type": "dart", "flutterMode": "debug", "program": "lib/main.dart", - "args": [ - "--dart-define", - "endpoint=http://localhost:8080", - ] + "args": [ + "--dart-define", + "endpoint=http://localhost:8080", + "--dart-define", + "web-family=http://localhost:3003" + ] }, ] } \ No newline at end of file diff --git a/fastlane/metadata/android/de/full_description.txt b/fastlane/metadata/android/de/full_description.txt index 66a4a1c2b..c20f3a53c 100644 --- a/fastlane/metadata/android/de/full_description.txt +++ b/fastlane/metadata/android/de/full_description.txt @@ -1,10 +1,10 @@ ente ist eine einfache App, um Ihre Fotos und Videos automatisch zu sichern und zu organisieren. -Wenn Sie auf der Suche nach einer privaten Alternative sind, um Ihre Erinnerungen zu bewahren, sind Sie an der richtigen Stelle. Mit Ente werden sie Ende-zu-Ende-verschlüsselt gespeichert (e2ee). Dies bedeutet, dass nur Sie sie sehen können. +Wenn Sie auf der Suche nach einer privaten Alternative sind, um Ihre Erinnerungen zu bewahren, sind Sie an der richtigen Stelle. Die müssen nicht mal ente haben. ente benötigt bestimmte Berechtigungen um als Anbieter eines Fotospeichers fungieren zu können. Mit Ente werden sie Ende-zu-Ende-verschlüsselt gespeichert (e2ee). Dies bedeutet, dass nur Sie sie sehen können. Wir haben Open-Source Apps auf allen Plattformen, und Ihre Fotos werden nahtlos zwischen all Ihren Geräten verschlüsselt (e2ee) synchronisiert. -ente ermöglicht es deine Alben simpel & schnell mit deinen Geliebten zu teilen. Die müssen nicht mal ente haben. Du kannst öffentlich einsehbare Links teilen, wo sie dein Album sehen und zusammenarbeiten können, indem sie Fotos hinzufügen, sogar ohne einen Account oder eine App. +ente ermöglicht es deine Alben simpel & schnell mit deinen Geliebten zu teilen. Du kannst öffentlich einsehbare Links teilen, wo sie dein Album sehen und zusammenarbeiten können, indem sie Fotos hinzufügen, sogar ohne einen Account oder eine App. Ihre verschlüsselten Daten werden zu 3 verschiedenen Orten repliziert, unter anderem zu einem Schutzbunker in Paris. Wir nehmen die Erhaltung der Nachwelt ernst und machen es Dir leicht, dafür zu sorgen, dass Deine Erinnerungen Dich überdauern. @@ -27,7 +27,7 @@ FEATURES - und noch VIELES mehr! BERECHTIGUNGEN -ente benötigt bestimmte Berechtigungen um als Anbieter eines Fotospeichers fungieren zu können. Diese können unter folgendem Link betrachtet werden: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md +Diese können unter folgendem Link betrachtet werden: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md PREIS Wir bieten keine lebenslang kostenlosen Abonnements an, da es für uns wichtig ist, einen nachhaltigen Service anzubieten. Wir bieten jedoch bezahlbare Abonemments an, welche auch mit der Familie geteilt werden können. Mehr Informationen sind auf ente.io zu finden. diff --git a/lib/db/files_db.dart b/lib/db/files_db.dart index 12f0c7881..2e7a760b5 100644 --- a/lib/db/files_db.dart +++ b/lib/db/files_db.dart @@ -1505,14 +1505,20 @@ class FilesDB { await batch.commit(noResult: true); } - Future> getAllFilesFromDB(Set collectionsToIgnore) async { + Future> getAllFilesFromDB( + Set collectionsToIgnore, { + bool dedupeByUploadId = true, + }) async { final db = await instance.database; final List> result = await db.query(filesTable, orderBy: '$columnCreationTime DESC'); final List files = convertToFiles(result); final List deduplicatedFiles = await applyDBFilters( files, - DBFilterOptions(ignoredCollectionIDs: collectionsToIgnore), + DBFilterOptions( + ignoredCollectionIDs: collectionsToIgnore, + dedupeUploadID: dedupeByUploadId, + ), ); return deduplicatedFiles; } diff --git a/lib/generated/intl/messages_cs.dart b/lib/generated/intl/messages_cs.dart index a0b3b9f7d..e91d3502e 100644 --- a/lib/generated/intl/messages_cs.dart +++ b/lib/generated/intl/messages_cs.dart @@ -24,15 +24,25 @@ class MessageLookup extends MessageLookupByLibrary { static Map _notInlinedMessages(_) => { "addToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Add to hidden album"), + "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( + "Change location of selected items?"), "contacts": MessageLookupByLibrary.simpleMessage("Contacts"), "deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage( "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."), + "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), + "editsToLocationWillOnlyBeSeenWithinEnte": + MessageLookupByLibrary.simpleMessage( + "Edits to location will only be seen within Ente"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "modifyYourQueryOrTrySearchingFor": MessageLookupByLibrary.simpleMessage( "Modify your query, or try searching for"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Move to hidden album"), + "selectALocation": + MessageLookupByLibrary.simpleMessage("Select a location"), + "selectALocationFirst": + MessageLookupByLibrary.simpleMessage("Select a location first"), "yourMap": MessageLookupByLibrary.simpleMessage("Your map") }; } diff --git a/lib/generated/intl/messages_de.dart b/lib/generated/intl/messages_de.dart index 9fd8013d1..3271c7968 100644 --- a/lib/generated/intl/messages_de.dart +++ b/lib/generated/intl/messages_de.dart @@ -393,6 +393,8 @@ class MessageLookup extends MessageLookupByLibrary { "centerPoint": MessageLookupByLibrary.simpleMessage("Mittelpunkt"), "changeEmail": MessageLookupByLibrary.simpleMessage("E-Mail-Adresse ändern"), + "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( + "Change location of selected items?"), "changePassword": MessageLookupByLibrary.simpleMessage("Passwort ändern"), "changePasswordTitle": @@ -599,10 +601,14 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m16, "duplicateItemsGroup": m17, "edit": MessageLookupByLibrary.simpleMessage("Bearbeiten"), + "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("Standort bearbeiten"), "editsSaved": MessageLookupByLibrary.simpleMessage("Änderungen gespeichert"), + "editsToLocationWillOnlyBeSeenWithinEnte": + MessageLookupByLibrary.simpleMessage( + "Edits to location will only be seen within Ente"), "eligible": MessageLookupByLibrary.simpleMessage("zulässig"), "email": MessageLookupByLibrary.simpleMessage("E-Mail"), "emailChangedTo": m18, @@ -1193,6 +1199,10 @@ class MessageLookup extends MessageLookupByLibrary { "Laden Sie Personen ein, damit Sie geteilte Fotos hier einsehen können"), "searchResultCount": m43, "security": MessageLookupByLibrary.simpleMessage("Sicherheit"), + "selectALocation": + MessageLookupByLibrary.simpleMessage("Select a location"), + "selectALocationFirst": + MessageLookupByLibrary.simpleMessage("Select a location first"), "selectAlbum": MessageLookupByLibrary.simpleMessage("Album auswählen"), "selectAll": MessageLookupByLibrary.simpleMessage("Alle markieren"), "selectFoldersForBackup": MessageLookupByLibrary.simpleMessage( @@ -1394,7 +1404,6 @@ class MessageLookup extends MessageLookupByLibrary { "Dadurch wirst du von folgendem Gerät abgemeldet:"), "thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage( "Dadurch wirst du von diesem Gerät abgemeldet!"), - "time": MessageLookupByLibrary.simpleMessage("Zeit"), "toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage("Foto oder Video verstecken"), "toResetVerifyEmail": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 4397e28d4..3eae25d3d 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -101,6 +101,9 @@ class MessageLookup extends MessageLookupByLibrary { static String m27(count, formattedSize) => "${Intl.plural(count, one: 'It can be deleted from the device to free up ${formattedSize}', other: 'They can be deleted from the device to free up ${formattedSize}')}"; + static String m67(currentlyProcessing, totalCount) => + "Processing ${currentlyProcessing} / ${totalCount}"; + static String m28(count) => "${Intl.plural(count, one: '${count} item', other: '${count} items')}"; @@ -379,6 +382,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Cannot delete shared files"), "centerPoint": MessageLookupByLibrary.simpleMessage("Center point"), "changeEmail": MessageLookupByLibrary.simpleMessage("Change email"), + "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( + "Change location of selected items?"), "changePassword": MessageLookupByLibrary.simpleMessage("Change password"), "changePasswordTitle": @@ -395,6 +400,8 @@ class MessageLookup extends MessageLookupByLibrary { "claimMore": MessageLookupByLibrary.simpleMessage("Claim more!"), "claimed": MessageLookupByLibrary.simpleMessage("Claimed"), "claimedStorageSoFar": m8, + "cleanUncategorized": + MessageLookupByLibrary.simpleMessage("Clean Uncategorized"), "clearCaches": MessageLookupByLibrary.simpleMessage("Clear caches"), "clearIndexes": MessageLookupByLibrary.simpleMessage("Clear indexes"), "click": MessageLookupByLibrary.simpleMessage("• Click"), @@ -581,9 +588,13 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m16, "duplicateItemsGroup": m17, "edit": MessageLookupByLibrary.simpleMessage("Edit"), + "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("Edit location"), "editsSaved": MessageLookupByLibrary.simpleMessage("Edits saved"), + "editsToLocationWillOnlyBeSeenWithinEnte": + MessageLookupByLibrary.simpleMessage( + "Edits to location will only be seen within Ente"), "eligible": MessageLookupByLibrary.simpleMessage("eligible"), "email": MessageLookupByLibrary.simpleMessage("Email"), "emailChangedTo": m18, @@ -709,6 +720,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("General"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generating encryption keys..."), + "genericProgress": m67, "goToSettings": MessageLookupByLibrary.simpleMessage("Go to settings"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( @@ -1151,6 +1163,10 @@ class MessageLookup extends MessageLookupByLibrary { "Invite people, and you\'ll see all photos shared by them here"), "searchResultCount": m43, "security": MessageLookupByLibrary.simpleMessage("Security"), + "selectALocation": + MessageLookupByLibrary.simpleMessage("Select a location"), + "selectALocationFirst": + MessageLookupByLibrary.simpleMessage("Select a location first"), "selectAlbum": MessageLookupByLibrary.simpleMessage("Select album"), "selectAll": MessageLookupByLibrary.simpleMessage("Select all"), "selectFoldersForBackup": @@ -1344,7 +1360,6 @@ class MessageLookup extends MessageLookupByLibrary { "This will log you out of the following device:"), "thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage( "This will log you out of this device!"), - "time": MessageLookupByLibrary.simpleMessage("Time"), "toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage("To hide a photo or video"), "toResetVerifyEmail": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_es.dart b/lib/generated/intl/messages_es.dart index 548fc33bc..1fd4f860f 100644 --- a/lib/generated/intl/messages_es.dart +++ b/lib/generated/intl/messages_es.dart @@ -336,6 +336,8 @@ class MessageLookup extends MessageLookupByLibrary { "centerPoint": MessageLookupByLibrary.simpleMessage("Punto central"), "changeEmail": MessageLookupByLibrary.simpleMessage("Cambiar correo electrónico"), + "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( + "Change location of selected items?"), "changePassword": MessageLookupByLibrary.simpleMessage("Cambiar contraseña"), "changePasswordTitle": @@ -527,10 +529,14 @@ class MessageLookup extends MessageLookupByLibrary { "dropSupportEmail": m15, "duplicateFileCountWithStorageSaved": m16, "edit": MessageLookupByLibrary.simpleMessage("Editar"), + "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("Editar la ubicación"), "editsSaved": MessageLookupByLibrary.simpleMessage("Ediciones guardadas"), + "editsToLocationWillOnlyBeSeenWithinEnte": + MessageLookupByLibrary.simpleMessage( + "Edits to location will only be seen within Ente"), "eligible": MessageLookupByLibrary.simpleMessage("elegible"), "email": MessageLookupByLibrary.simpleMessage("Correo electrónico"), "emailChangedTo": m18, @@ -1032,6 +1038,10 @@ class MessageLookup extends MessageLookupByLibrary { "searchHintText": MessageLookupByLibrary.simpleMessage( "Álbunes, meses, días, años, ..."), "security": MessageLookupByLibrary.simpleMessage("Seguridad"), + "selectALocation": + MessageLookupByLibrary.simpleMessage("Select a location"), + "selectALocationFirst": + MessageLookupByLibrary.simpleMessage("Select a location first"), "selectAlbum": MessageLookupByLibrary.simpleMessage("Seleccionar álbum"), "selectAll": MessageLookupByLibrary.simpleMessage("Seleccionar todos"), @@ -1208,7 +1218,6 @@ class MessageLookup extends MessageLookupByLibrary { "Esto cerrará la sesión del siguiente dispositivo:"), "thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage( "¡Esto cerrará la sesión de este dispositivo!"), - "time": MessageLookupByLibrary.simpleMessage("Tiempo"), "toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage( "Para ocultar una foto o video"), "todaysLogs": MessageLookupByLibrary.simpleMessage("Registros de hoy"), diff --git a/lib/generated/intl/messages_fr.dart b/lib/generated/intl/messages_fr.dart index 50dbaf3f4..2239a904f 100644 --- a/lib/generated/intl/messages_fr.dart +++ b/lib/generated/intl/messages_fr.dart @@ -392,6 +392,8 @@ class MessageLookup extends MessageLookupByLibrary { "centerPoint": MessageLookupByLibrary.simpleMessage("Point central"), "changeEmail": MessageLookupByLibrary.simpleMessage("Modifier l\'e-mail"), + "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( + "Change location of selected items?"), "changePassword": MessageLookupByLibrary.simpleMessage("Modifier le mot de passe"), "changePasswordTitle": @@ -603,10 +605,14 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m16, "duplicateItemsGroup": m17, "edit": MessageLookupByLibrary.simpleMessage("Éditer"), + "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("Modifier l’emplacement"), "editsSaved": MessageLookupByLibrary.simpleMessage("Modification sauvegardée"), + "editsToLocationWillOnlyBeSeenWithinEnte": + MessageLookupByLibrary.simpleMessage( + "Edits to location will only be seen within Ente"), "eligible": MessageLookupByLibrary.simpleMessage("éligible"), "email": MessageLookupByLibrary.simpleMessage("E-mail"), "emailChangedTo": m18, @@ -1195,6 +1201,10 @@ class MessageLookup extends MessageLookupByLibrary { "Invitez des gens, et vous verrez ici toutes les photos qu\'ils partagent"), "searchResultCount": m43, "security": MessageLookupByLibrary.simpleMessage("Sécurité"), + "selectALocation": + MessageLookupByLibrary.simpleMessage("Select a location"), + "selectALocationFirst": + MessageLookupByLibrary.simpleMessage("Select a location first"), "selectAlbum": MessageLookupByLibrary.simpleMessage("Sélectionner album"), "selectAll": MessageLookupByLibrary.simpleMessage("Tout sélectionner"), @@ -1396,7 +1406,6 @@ class MessageLookup extends MessageLookupByLibrary { "Cela vous déconnectera de l\'appareil suivant :"), "thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage( "Cela vous déconnectera de cet appareil !"), - "time": MessageLookupByLibrary.simpleMessage("Date et heure"), "toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage( "Cacher une photo ou une vidéo"), "toResetVerifyEmail": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_it.dart b/lib/generated/intl/messages_it.dart index 761747c48..8ecbf58b8 100644 --- a/lib/generated/intl/messages_it.dart +++ b/lib/generated/intl/messages_it.dart @@ -378,6 +378,8 @@ class MessageLookup extends MessageLookupByLibrary { "Impossibile eliminare i file condivisi"), "centerPoint": MessageLookupByLibrary.simpleMessage("Punto centrale"), "changeEmail": MessageLookupByLibrary.simpleMessage("Modifica email"), + "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( + "Change location of selected items?"), "changePassword": MessageLookupByLibrary.simpleMessage("Cambia password"), "changePasswordTitle": @@ -583,9 +585,13 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m16, "duplicateItemsGroup": m17, "edit": MessageLookupByLibrary.simpleMessage("Modifica"), + "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("Modifica luogo"), "editsSaved": MessageLookupByLibrary.simpleMessage("Modifiche salvate"), + "editsToLocationWillOnlyBeSeenWithinEnte": + MessageLookupByLibrary.simpleMessage( + "Edits to location will only be seen within Ente"), "eligible": MessageLookupByLibrary.simpleMessage("idoneo"), "email": MessageLookupByLibrary.simpleMessage("Email"), "emailChangedTo": m18, @@ -1126,6 +1132,10 @@ class MessageLookup extends MessageLookupByLibrary { "searchHintText": MessageLookupByLibrary.simpleMessage( "Album, mesi, giorni, anni, ..."), "security": MessageLookupByLibrary.simpleMessage("Sicurezza"), + "selectALocation": + MessageLookupByLibrary.simpleMessage("Select a location"), + "selectALocationFirst": + MessageLookupByLibrary.simpleMessage("Select a location first"), "selectAlbum": MessageLookupByLibrary.simpleMessage("Seleziona album"), "selectAll": MessageLookupByLibrary.simpleMessage("Seleziona tutto"), "selectFoldersForBackup": MessageLookupByLibrary.simpleMessage( @@ -1325,7 +1335,6 @@ class MessageLookup extends MessageLookupByLibrary { "Verrai disconnesso dai seguenti dispositivi:"), "thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage( "Verrai disconnesso dal tuo dispositivo!"), - "time": MessageLookupByLibrary.simpleMessage("Ora"), "toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage( "Per nascondere una foto o un video"), "toResetVerifyEmail": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_ko.dart b/lib/generated/intl/messages_ko.dart index 7c5259342..d9c3fcd9e 100644 --- a/lib/generated/intl/messages_ko.dart +++ b/lib/generated/intl/messages_ko.dart @@ -24,15 +24,25 @@ class MessageLookup extends MessageLookupByLibrary { static Map _notInlinedMessages(_) => { "addToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Add to hidden album"), + "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( + "Change location of selected items?"), "contacts": MessageLookupByLibrary.simpleMessage("Contacts"), "deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage( "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."), + "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), + "editsToLocationWillOnlyBeSeenWithinEnte": + MessageLookupByLibrary.simpleMessage( + "Edits to location will only be seen within Ente"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "modifyYourQueryOrTrySearchingFor": MessageLookupByLibrary.simpleMessage( "Modify your query, or try searching for"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Move to hidden album"), + "selectALocation": + MessageLookupByLibrary.simpleMessage("Select a location"), + "selectALocationFirst": + MessageLookupByLibrary.simpleMessage("Select a location first"), "yourMap": MessageLookupByLibrary.simpleMessage("Your map") }; } diff --git a/lib/generated/intl/messages_nl.dart b/lib/generated/intl/messages_nl.dart index 5d83fc640..cd612d97b 100644 --- a/lib/generated/intl/messages_nl.dart +++ b/lib/generated/intl/messages_nl.dart @@ -391,6 +391,8 @@ class MessageLookup extends MessageLookupByLibrary { "Kan gedeelde bestanden niet verwijderen"), "centerPoint": MessageLookupByLibrary.simpleMessage("Middelpunt"), "changeEmail": MessageLookupByLibrary.simpleMessage("E-mail wijzigen"), + "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( + "Change location of selected items?"), "changePassword": MessageLookupByLibrary.simpleMessage("Wachtwoord wijzigen"), "changePasswordTitle": @@ -597,10 +599,14 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m16, "duplicateItemsGroup": m17, "edit": MessageLookupByLibrary.simpleMessage("Bewerken"), + "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("Locatie bewerken"), "editsSaved": MessageLookupByLibrary.simpleMessage("Bewerkingen opgeslagen"), + "editsToLocationWillOnlyBeSeenWithinEnte": + MessageLookupByLibrary.simpleMessage( + "Edits to location will only be seen within Ente"), "eligible": MessageLookupByLibrary.simpleMessage("gerechtigd"), "email": MessageLookupByLibrary.simpleMessage("E-mail"), "emailChangedTo": m18, @@ -1190,6 +1196,10 @@ class MessageLookup extends MessageLookupByLibrary { "Nodig mensen uit, en je ziet alle foto\'s die door hen worden gedeeld hier"), "searchResultCount": m43, "security": MessageLookupByLibrary.simpleMessage("Beveiliging"), + "selectALocation": + MessageLookupByLibrary.simpleMessage("Select a location"), + "selectALocationFirst": + MessageLookupByLibrary.simpleMessage("Select a location first"), "selectAlbum": MessageLookupByLibrary.simpleMessage("Album selecteren"), "selectAll": MessageLookupByLibrary.simpleMessage("Selecteer alles"), "selectFoldersForBackup": MessageLookupByLibrary.simpleMessage( @@ -1387,7 +1397,6 @@ class MessageLookup extends MessageLookupByLibrary { "Dit zal je uitloggen van het volgende apparaat:"), "thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage( "Dit zal je uitloggen van dit apparaat!"), - "time": MessageLookupByLibrary.simpleMessage("Tijd"), "toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage( "Om een foto of video te verbergen"), "toResetVerifyEmail": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_no.dart b/lib/generated/intl/messages_no.dart index d162c4550..97bb1d5ea 100644 --- a/lib/generated/intl/messages_no.dart +++ b/lib/generated/intl/messages_no.dart @@ -29,6 +29,8 @@ class MessageLookup extends MessageLookupByLibrary { "askDeleteReason": MessageLookupByLibrary.simpleMessage( "Hva er hovedårsaken til at du sletter kontoen din?"), "cancel": MessageLookupByLibrary.simpleMessage("Avbryt"), + "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( + "Change location of selected items?"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage("Bekreft sletting av konto"), "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( @@ -39,6 +41,10 @@ class MessageLookup extends MessageLookupByLibrary { "Vi er lei oss for at du forlater oss. Gi oss gjerne en tilbakemelding så vi kan forbedre oss."), "deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage( "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."), + "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), + "editsToLocationWillOnlyBeSeenWithinEnte": + MessageLookupByLibrary.simpleMessage( + "Edits to location will only be seen within Ente"), "email": MessageLookupByLibrary.simpleMessage("E-post"), "enterValidEmail": MessageLookupByLibrary.simpleMessage( "Vennligst skriv inn en gyldig e-postadresse."), @@ -55,6 +61,10 @@ class MessageLookup extends MessageLookupByLibrary { "Modify your query, or try searching for"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Move to hidden album"), + "selectALocation": + MessageLookupByLibrary.simpleMessage("Select a location"), + "selectALocationFirst": + MessageLookupByLibrary.simpleMessage("Select a location first"), "verify": MessageLookupByLibrary.simpleMessage("Bekreft"), "yourMap": MessageLookupByLibrary.simpleMessage("Your map") }; diff --git a/lib/generated/intl/messages_pl.dart b/lib/generated/intl/messages_pl.dart index f7cb0acff..7f3b6505f 100644 --- a/lib/generated/intl/messages_pl.dart +++ b/lib/generated/intl/messages_pl.dart @@ -35,6 +35,8 @@ class MessageLookup extends MessageLookupByLibrary { "cancel": MessageLookupByLibrary.simpleMessage("Anuluj"), "changeEmail": MessageLookupByLibrary.simpleMessage("Zmień adres e-mail"), + "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( + "Change location of selected items?"), "changePasswordTitle": MessageLookupByLibrary.simpleMessage("Zmień hasło"), "checkInboxAndSpamFolder": MessageLookupByLibrary.simpleMessage( @@ -77,6 +79,10 @@ class MessageLookup extends MessageLookupByLibrary { "deleteRequestSLAText": MessageLookupByLibrary.simpleMessage( "Twoje żądanie zostanie przetworzone w ciągu 72 godzin."), "doThisLater": MessageLookupByLibrary.simpleMessage("Spróbuj później"), + "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), + "editsToLocationWillOnlyBeSeenWithinEnte": + MessageLookupByLibrary.simpleMessage( + "Edits to location will only be seen within Ente"), "email": MessageLookupByLibrary.simpleMessage("Adres e-mail"), "encryption": MessageLookupByLibrary.simpleMessage("Szyfrowanie"), "enterCode": MessageLookupByLibrary.simpleMessage("Wprowadź kod"), @@ -149,6 +155,10 @@ class MessageLookup extends MessageLookupByLibrary { "resetPasswordTitle": MessageLookupByLibrary.simpleMessage("Zresetuj hasło"), "saveKey": MessageLookupByLibrary.simpleMessage("Zapisz klucz"), + "selectALocation": + MessageLookupByLibrary.simpleMessage("Select a location"), + "selectALocationFirst": + MessageLookupByLibrary.simpleMessage("Select a location first"), "selectReason": MessageLookupByLibrary.simpleMessage("Wybierz powód"), "sendEmail": MessageLookupByLibrary.simpleMessage("Wyślij e-mail"), "setPasswordTitle": MessageLookupByLibrary.simpleMessage("Ustaw hasło"), diff --git a/lib/generated/intl/messages_pt.dart b/lib/generated/intl/messages_pt.dart index bc38ae728..1f8f26cc1 100644 --- a/lib/generated/intl/messages_pt.dart +++ b/lib/generated/intl/messages_pt.dart @@ -102,6 +102,8 @@ class MessageLookup extends MessageLookupByLibrary { "cancel": MessageLookupByLibrary.simpleMessage("Cancelar"), "cannotAddMorePhotosAfterBecomingViewer": m7, "changeEmail": MessageLookupByLibrary.simpleMessage("Mudar e-mail"), + "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( + "Change location of selected items?"), "changePassword": MessageLookupByLibrary.simpleMessage("Mude sua senha"), "changePasswordTitle": @@ -186,6 +188,10 @@ class MessageLookup extends MessageLookupByLibrary { "doThisLater": MessageLookupByLibrary.simpleMessage("Fazer isso mais tarde"), "dropSupportEmail": m15, + "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), + "editsToLocationWillOnlyBeSeenWithinEnte": + MessageLookupByLibrary.simpleMessage( + "Edits to location will only be seen within Ente"), "eligible": MessageLookupByLibrary.simpleMessage("elegível"), "email": MessageLookupByLibrary.simpleMessage("E-mail"), "encryption": MessageLookupByLibrary.simpleMessage("Criptografia"), @@ -350,6 +356,10 @@ class MessageLookup extends MessageLookupByLibrary { "scanThisBarcodeWithnyourAuthenticatorApp": MessageLookupByLibrary.simpleMessage( "Escaneie este código de barras com\nseu aplicativo autenticador"), + "selectALocation": + MessageLookupByLibrary.simpleMessage("Select a location"), + "selectALocationFirst": + MessageLookupByLibrary.simpleMessage("Select a location first"), "selectReason": MessageLookupByLibrary.simpleMessage("Selecione o motivo"), "sendEmail": MessageLookupByLibrary.simpleMessage("Enviar e-mail"), diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart index 7ef02f16b..fe8af3702 100644 --- a/lib/generated/intl/messages_zh.dart +++ b/lib/generated/intl/messages_zh.dart @@ -96,6 +96,9 @@ class MessageLookup extends MessageLookupByLibrary { static String m27(count, formattedSize) => "${Intl.plural(count, one: '它可以从设备中删除以释放 ${formattedSize}', other: '它们可以从设备中删除以释放 ${formattedSize}')}"; + static String m67(currentlyProcessing, totalCount) => + "正在处理 ${currentlyProcessing} / ${totalCount}"; + static String m28(count) => "${Intl.plural(count, one: '${count} 个项目', other: '${count} 个项目')}"; @@ -335,6 +338,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("无法删除共享文件"), "centerPoint": MessageLookupByLibrary.simpleMessage("中心点"), "changeEmail": MessageLookupByLibrary.simpleMessage("修改邮箱"), + "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( + "Change location of selected items?"), "changePassword": MessageLookupByLibrary.simpleMessage("修改密码"), "changePasswordTitle": MessageLookupByLibrary.simpleMessage("修改密码"), "changePermissions": MessageLookupByLibrary.simpleMessage("要修改权限吗?"), @@ -346,6 +351,7 @@ class MessageLookup extends MessageLookupByLibrary { "claimMore": MessageLookupByLibrary.simpleMessage("领取更多!"), "claimed": MessageLookupByLibrary.simpleMessage("已领取"), "claimedStorageSoFar": m8, + "cleanUncategorized": MessageLookupByLibrary.simpleMessage("清除未分类的"), "clearCaches": MessageLookupByLibrary.simpleMessage("清除缓存"), "click": MessageLookupByLibrary.simpleMessage("• 点击"), "clickOnTheOverflowMenu": @@ -492,8 +498,12 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m16, "duplicateItemsGroup": m17, "edit": MessageLookupByLibrary.simpleMessage("编辑"), + "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("编辑位置"), "editsSaved": MessageLookupByLibrary.simpleMessage("已保存编辑"), + "editsToLocationWillOnlyBeSeenWithinEnte": + MessageLookupByLibrary.simpleMessage( + "Edits to location will only be seen within Ente"), "eligible": MessageLookupByLibrary.simpleMessage("符合资格"), "email": MessageLookupByLibrary.simpleMessage("电子邮件地址"), "emailChangedTo": m18, @@ -596,6 +606,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("通用"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage("正在生成加密密钥..."), + "genericProgress": m67, "goToSettings": MessageLookupByLibrary.simpleMessage("前往设置"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), "grantFullAccessPrompt": @@ -958,6 +969,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("邀请他人,您将在此看到他们分享的所有照片"), "searchResultCount": m43, "security": MessageLookupByLibrary.simpleMessage("安全"), + "selectALocation": + MessageLookupByLibrary.simpleMessage("Select a location"), + "selectALocationFirst": + MessageLookupByLibrary.simpleMessage("Select a location first"), "selectAlbum": MessageLookupByLibrary.simpleMessage("选择相册"), "selectAll": MessageLookupByLibrary.simpleMessage("全选"), "selectFoldersForBackup": @@ -1121,7 +1136,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("这将使您在以下设备中退出登录:"), "thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage("这将使您在此设备上退出登录!"), - "time": MessageLookupByLibrary.simpleMessage("时间"), "toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage("隐藏照片或视频"), "toResetVerifyEmail": MessageLookupByLibrary.simpleMessage("要重置您的密码,请先验证您的电子邮件。"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index f861f8b5b..5765cf70b 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -4976,6 +4976,16 @@ class S { ); } + /// `Processing {currentlyProcessing} / {totalCount}` + String genericProgress(int currentlyProcessing, int totalCount) { + return Intl.message( + 'Processing $currentlyProcessing / $totalCount', + name: 'genericProgress', + desc: 'Generic progress text to display when processing multiple items', + args: [currentlyProcessing, totalCount], + ); + } + /// `Permanently delete` String get permanentlyDelete { return Intl.message( @@ -6035,16 +6045,6 @@ class S { ); } - /// `Time` - String get time { - return Intl.message( - 'Time', - name: 'time', - desc: '', - args: [], - ); - } - /// `Long-press on an item to view in full-screen` String get longpressOnAnItemToViewInFullscreen { return Intl.message( @@ -8227,6 +8227,66 @@ class S { args: [], ); } + + /// `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( + 'Clean Uncategorized', + name: 'cleanUncategorized', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 515bfae5a..6a71af50f 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 086da1998..5549ff9ef 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index d51611b2c..9bb80a822 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -703,6 +703,22 @@ "deleteEmptyAlbumsWithQuestionMark": "Delete empty albums?", "deleteAlbumsDialogBody": "This will delete all empty albums. This is useful when you want to reduce the clutter in your album list.", "deleteProgress": "Deleting {currentlyDeleting} / {totalCount}", + "genericProgress": "Processing {currentlyProcessing} / {totalCount}", + "@genericProgress" : { + "description": "Generic progress text to display when processing multiple items", + "type": "text", + "placeholders": { + "currentlyProcessing": { + "example": "1", + "type": "int" + }, + "totalCount": { + "example": "10", + "type": "int" + } + } + }, + "permanentlyDelete": "Permanently delete", "canOnlyCreateLinkForFilesOwnedByYou": "Can only create link for files owned by you", "publicLinkCreated": "Public link created", @@ -830,7 +846,6 @@ "clubByFileName": "Club by file name", "count": "Count", "totalSize": "Total size", - "time": "Time", "longpressOnAnItemToViewInFullscreen": "Long-press on an item to view in full-screen", "decryptingVideo": "Decrypting video...", "authToViewYourMemories": "Please authenticate to view your memories", @@ -936,8 +951,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", @@ -1162,9 +1177,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" + "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" } \ No newline at end of file diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index d7a9aabd8..0720f8d3e 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index deab1cabe..fe9c4c64c 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index d82d8e682..7df3892f5 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/intl_ko.arb b/lib/l10n/intl_ko.arb index 515bfae5a..6a71af50f 100644 --- a/lib/l10n/intl_ko.arb +++ b/lib/l10n/intl_ko.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/intl_nl.arb b/lib/l10n/intl_nl.arb index b4d60d451..bb8404dc4 100644 --- a/lib/l10n/intl_nl.arb +++ b/lib/l10n/intl_nl.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/intl_no.arb b/lib/l10n/intl_no.arb index 8106b8ca7..d55787f17 100644 --- a/lib/l10n/intl_no.arb +++ b/lib/l10n/intl_no.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index 306b0a31c..7dfb7abc1 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index cd674a347..e0232c58e 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index a445e3e8e..7997edbbe 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -696,6 +696,21 @@ "deleteEmptyAlbumsWithQuestionMark": "要删除空相册吗?", "deleteAlbumsDialogBody": "这将删除所有空相册。 当您想减少相册列表中的混乱时,这很有用。", "deleteProgress": "正在删除 {currentlyDeleting} /共 {totalCount}", + "genericProgress": "正在处理 {currentlyProcessing} / {totalCount}", + "@genericProgress": { + "description": "Generic progress text to display when processing multiple items", + "type": "text", + "placeholders": { + "currentlyProcessing": { + "example": "1", + "type": "int" + }, + "totalCount": { + "example": "10", + "type": "int" + } + } + }, "permanentlyDelete": "永久删除", "canOnlyCreateLinkForFilesOwnedByYou": "只能为您拥有的文件创建链接", "publicLinkCreated": "公共链接已创建", @@ -823,7 +838,6 @@ "clubByFileName": "按文件名排序", "count": "计数", "totalSize": "总大小", - "time": "时间", "longpressOnAnItemToViewInFullscreen": "长按一个项目来全屏查看", "decryptingVideo": "正在解密视频...", "authToViewYourMemories": "请验证以查看您的回忆", @@ -1158,5 +1172,11 @@ "signOutFromOtherDevices": "从其他设备退出登录", "signOutOtherBody": "如果你认为有人可能知道你的密码,你可以强制所有使用你账户的其他设备退出登录。", "signOutOtherDevices": "登出其他设备", - "doNotSignOut": "不要退登" + "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": "清除未分类的" } \ No newline at end of file diff --git a/lib/models/duplicate_files.dart b/lib/models/duplicate_files.dart index a0d363e07..8bb538859 100644 --- a/lib/models/duplicate_files.dart +++ b/lib/models/duplicate_files.dart @@ -4,38 +4,48 @@ import 'package:photos/models/file/file.dart'; import 'package:photos/services/collections_service.dart'; class DuplicateFilesResponse { - final List duplicates; - DuplicateFilesResponse(this.duplicates); + final List sameSizeFiles; + DuplicateFilesResponse(this.sameSizeFiles); factory DuplicateFilesResponse.fromMap(Map map) { return DuplicateFilesResponse( - List.from( - map['duplicates']?.map((x) => DuplicateItems.fromMap(x)), + List.from( + map['duplicates']?.map((x) => FileWithSameSize.fromMap(x)), ), ); } + Map toUploadIDToSize() { + final Map result = {}; + for (final filesWithSameSize in sameSizeFiles) { + for (final uploadID in filesWithSameSize.fileIDs) { + result[uploadID] = filesWithSameSize.size; + } + } + return result; + } + factory DuplicateFilesResponse.fromJson(String source) => DuplicateFilesResponse.fromMap(json.decode(source)); @override - String toString() => 'DuplicateFiles(duplicates: $duplicates)'; + String toString() => 'DuplicateFiles(sameSizeFiles: $sameSizeFiles)'; } -class DuplicateItems { +class FileWithSameSize { final List fileIDs; final int size; - DuplicateItems(this.fileIDs, this.size); + FileWithSameSize(this.fileIDs, this.size); - factory DuplicateItems.fromMap(Map map) { - return DuplicateItems( + factory FileWithSameSize.fromMap(Map map) { + return FileWithSameSize( List.from(map['fileIDs']), map['size'], ); } - factory DuplicateItems.fromJson(String source) => - DuplicateItems.fromMap(json.decode(source)); + factory FileWithSameSize.fromJson(String source) => + FileWithSameSize.fromMap(json.decode(source)); @override String toString() => 'Duplicates(fileIDs: $fileIDs, size: $size)'; @@ -44,11 +54,30 @@ class DuplicateItems { class DuplicateFiles { final List files; final int size; + final Set collectionIDs; static final collectionsService = CollectionsService.instance; - DuplicateFiles(this.files, this.size) { + DuplicateFiles( + this.files, + this.size, + this.collectionIDs, + ) { sortByCollectionName(); } + // sortByLocalIDs sorts the files such that files with localID are at the top + List sortByLocalIDs() { + final List filesWithoutLocalID = []; + final List localFiles = []; + for (final file in files) { + if ((file.localID ?? '').isEmpty) { + localFiles.add(file); + } else { + filesWithoutLocalID.add(file); + } + } + localFiles.addAll(filesWithoutLocalID); + return localFiles; + } @override String toString() => 'DuplicateFiles(files: $files, size: $size)'; diff --git a/lib/models/file/file.dart b/lib/models/file/file.dart index a0481cd39..208cfa790 100644 --- a/lib/models/file/file.dart +++ b/lib/models/file/file.dart @@ -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; } diff --git a/lib/models/gallery_type.dart b/lib/models/gallery_type.dart index e3942345d..ba0eb397f 100644 --- a/lib/models/gallery_type.dart +++ b/lib/models/gallery_type.dart @@ -242,6 +242,10 @@ extension GalleyTypeExtension on GalleryType { bool showRemoveFromHiddenAlbum() { return this == GalleryType.hiddenOwnedCollection; } + + bool showEditLocation() { + return this != GalleryType.sharedCollection; + } } extension GalleryAppBarExtn on GalleryType { diff --git a/lib/services/billing_service.dart b/lib/services/billing_service.dart index d565149fc..e9485f50c 100644 --- a/lib/services/billing_service.dart +++ b/lib/services/billing_service.dart @@ -184,6 +184,7 @@ class BillingService { try { final String jwtToken = await UserService.instance.getFamiliesToken(); final bool familyExist = userDetails.isPartOfFamily(); + await dialog.hide(); await Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { @@ -198,6 +199,5 @@ class BillingService { await dialog.hide(); await showGenericErrorDialog(context: context, error: e); } - await dialog.hide(); } } diff --git a/lib/services/collections_service.dart b/lib/services/collections_service.dart index ab6f7379c..fc51ced3c 100644 --- a/lib/services/collections_service.dart +++ b/lib/services/collections_service.dart @@ -310,6 +310,21 @@ class CollectionsService { .toList(); } + // getActiveCollections returns list of collections which are not deleted yet + Set nonHiddenOwnedCollections() { + final int ownerID = _config.getUserID()!; + return _collectionIDToCollections.values + .toList() + .where( + (element) => + !element.isDeleted && + !element.isHidden() && + element.isOwner(ownerID), + ) + .map((e) => e.id) + .toSet(); + } + // returns collections after removing deleted,uncategorized, and hidden // collections List getCollectionsForUI({ @@ -1173,6 +1188,64 @@ class CollectionsService { } } + // This method is used to add files to a collection without firing any events. + // Unlike `addToCollection`, this method does not update the `FilesDB` or modify + // the `EnteFile` objects passed to it. This is only used during dedupe process + // for adding files to a collection without firing any events. + Future addSilentlyToCollection( + int collectionID, + List files, + ) async { + if (files.isEmpty) { + return; + } + // as any non uploaded file + final pendingUpload = files.any( + (element) => element.uploadedFileID == null, + ); + if (pendingUpload) { + throw ArgumentError('Can only add uploaded files silently'); + } + final existingFileIDsInCollection = + await FilesDB.instance.getUploadedFileIDs(collectionID); + files.removeWhere( + (element) => existingFileIDsInCollection.contains(element.uploadedFileID), + ); + if (files.isEmpty) { + _logger.info("nothing to add to the collection"); + return; + } + final params = {}; + params["collectionID"] = collectionID; + final batchedFiles = files.chunks(batchSize); + for (final batch in batchedFiles) { + params["files"] = []; + for (final file in batch) { + final int uploadedFileID = file.uploadedFileID!; + final fileKey = getFileKey(file); + final encryptedKeyData = + CryptoUtil.encryptSync(fileKey, getCollectionKey(collectionID)); + final String encryptedKey = + CryptoUtil.bin2base64(encryptedKeyData.encryptedData!); + final String keyDecryptionNonce = + CryptoUtil.bin2base64(encryptedKeyData.nonce!); + params["files"].add( + CollectionFileItem(uploadedFileID, encryptedKey, keyDecryptionNonce) + .toMap(), + ); + } + try { + await _enteDio.post( + "/collections/add-files", + data: params, + ); + } catch (e) { + _logger.warning('failed to add files to collection', e); + rethrow; + } + } + } + Future linkLocalFileToExistingUploadedFileInAnotherCollection( int destCollectionID, { required EnteFile localFileToUpload, diff --git a/lib/services/deduplication_service.dart b/lib/services/deduplication_service.dart index 7af257c75..41ddff091 100644 --- a/lib/services/deduplication_service.dart +++ b/lib/services/deduplication_service.dart @@ -1,6 +1,5 @@ import 'package:logging/logging.dart'; import "package:photos/core/configuration.dart"; -import 'package:photos/core/errors.dart'; import 'package:photos/core/network/network.dart'; import 'package:photos/db/files_db.dart'; import 'package:photos/models/duplicate_files.dart'; @@ -19,54 +18,7 @@ class DeduplicationService { Future> getDuplicateFiles() async { try { - final bool hasFileSizes = await FilesService.instance.hasMigratedSizes(); - if (hasFileSizes) { - final List result = await _getDuplicateFilesFromLocal(); - return result; - } - final DuplicateFilesResponse dupes = await _fetchDuplicateFileIDs(); - final ids = []; - for (final dupe in dupes.duplicates) { - ids.addAll(dupe.fileIDs); - } - final fileMap = await FilesDB.instance.getFilesFromIDs(ids); - final result = []; - final missingFileIDs = []; - for (final dupe in dupes.duplicates) { - final files = []; - for (final id in dupe.fileIDs) { - final file = fileMap[id]; - if (file != null) { - files.add(file); - } else { - missingFileIDs.add(id); - } - } - // Place files that are available locally at first to minimize the chances - // of a deletion followed by a re-upload - files.sort((first, second) { - if (first.localID != null && second.localID == null) { - return -1; - } else if (first.localID == null && second.localID != null) { - return 1; - } - return 0; - }); - if (files.length > 1) { - result.add(DuplicateFiles(files, dupe.size)); - } - } - if (missingFileIDs.isNotEmpty) { - _logger.severe( - "Missing files", - InvalidStateError( - "Could not find " + - missingFileIDs.length.toString() + - " files in local DB: " + - missingFileIDs.toString(), - ), - ); - } + final List result = await _getDuplicateFiles(); return result; } catch (e, s) { _logger.severe("failed to get dedupeFile", e, s); @@ -74,61 +26,65 @@ class DeduplicationService { } } - List clubDuplicates( - List dupesBySize, { - required String? Function(EnteFile) clubbingKey, - }) { - final dupesBySizeAndClubKey = []; - for (final sizeBasedDupe in dupesBySize) { - final Map> clubKeyToFilesMap = {}; - for (final file in sizeBasedDupe.files) { - final String? clubKey = clubbingKey(file); - if (clubKey == null || clubKey.isEmpty) { - continue; - } - if (!clubKeyToFilesMap.containsKey(clubKey)) { - clubKeyToFilesMap[clubKey] = []; - } - clubKeyToFilesMap[clubKey]!.add(file); - } - for (final clubbingKey in clubKeyToFilesMap.keys) { - final clubbedFiles = clubKeyToFilesMap[clubbingKey]!; - if (clubbedFiles.length > 1) { - dupesBySizeAndClubKey.add( - DuplicateFiles(clubbedFiles, sizeBasedDupe.size), - ); - } - } + // Returns a list of DuplicateFiles, where each DuplicateFiles object contains + // a list of files that have the same size and hash + Future> _getDuplicateFiles() async { + Map uploadIDToSize = {}; + final bool hasFileSizes = await FilesService.instance.hasMigratedSizes(); + if (!hasFileSizes) { + final DuplicateFilesResponse dupes = await _fetchDuplicateFileIDs(); + uploadIDToSize = dupes.toUploadIDToSize(); } - return dupesBySizeAndClubKey; - } + final Set allowedCollectionIDs = + CollectionsService.instance.nonHiddenOwnedCollections(); - Future> _getDuplicateFilesFromLocal() async { final List allFiles = await FilesDB.instance.getAllFilesFromDB( CollectionsService.instance.getHiddenCollectionIds(), + dedupeByUploadId: false, ); final int ownerID = Configuration.instance.getUserID()!; - allFiles.removeWhere( - (f) => - !f.isUploaded || - (f.ownerID ?? 0) != ownerID || - (f.fileSize ?? 0) <= 0, - ); - final Map> sizeToFilesMap = {}; + final List filteredFiles = []; for (final file in allFiles) { - if (!sizeToFilesMap.containsKey(file.fileSize)) { - sizeToFilesMap[file.fileSize!] = []; + if (!file.isUploaded || + (file.hash ?? '').isEmpty || + (file.ownerID ?? 0) != ownerID || + (!allowedCollectionIDs.contains(file.collectionID!))) { + continue; } - sizeToFilesMap[file.fileSize]!.add(file); + if ((file.fileSize ?? 0) <= 0) { + file.fileSize = uploadIDToSize[file.uploadedFileID!] ?? 0; + } + if ((file.fileSize ?? 0) <= 0) { + continue; + } + filteredFiles.add(file); } - final List dupesBySize = []; - for (final size in sizeToFilesMap.keys) { - final List files = sizeToFilesMap[size]!; + + final Map> sizeHashToFilesMap = {}; + final Map> sizeHashToCollectionsSet = {}; + final Set processedFileIds = {}; + for (final file in filteredFiles) { + final key = '${file.fileSize}-${file.hash}'; + if (!sizeHashToFilesMap.containsKey(key)) { + sizeHashToFilesMap[key] = []; + sizeHashToCollectionsSet[key] = {}; + } + sizeHashToCollectionsSet[key]!.add(file.collectionID!); + if (!processedFileIds.contains(file.uploadedFileID)) { + sizeHashToFilesMap[key]!.add(file); + processedFileIds.add(file.uploadedFileID!); + } + } + final List dupesBySizeHash = []; + for (final key in sizeHashToFilesMap.keys) { + final List files = sizeHashToFilesMap[key]!; + final Set collectionIds = sizeHashToCollectionsSet[key]!; if (files.length > 1) { - dupesBySize.add(DuplicateFiles(files, size)); + final size = files[0].fileSize!; + dupesBySizeHash.add(DuplicateFiles(files, size, collectionIds)); } } - return dupesBySize; + return dupesBySizeHash; } Future _fetchDuplicateFileIDs() async { diff --git a/lib/services/files_service.dart b/lib/services/files_service.dart index d97b8a25e..a72db91a9 100644 --- a/lib/services/files_service.dart +++ b/lib/services/files_service.dart @@ -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 bulkEditLocationData( + List files, + LatLng location, + BuildContext context, + ) async { + final List uploadedFiles = + files.where((element) => element.uploadedFileID != null).toList(); + + final List remoteFilesToUpdate = []; + final Map> 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 _editLocationData( + List uploadedFiles, + Map> fileIDToUpdateMetadata, + List 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 bulkEditTime( diff --git a/lib/services/location_service.dart b/lib/services/location_service.dart index 2bef2ef41..10e3bfd72 100644 --- a/lib/services/location_service.dart +++ b/lib/services/location_service.dart @@ -114,17 +114,25 @@ class LocationService { return false; } - String convertLocationToDMS(Location centerPoint) { + /// returns [lat, lng] + List? 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 convertCoordinateToDMS(double coordinate) { + List _convertCoordinateToDMS(double coordinate) { final degrees = coordinate.floor(); final minutes = ((coordinate - degrees) * 60).floor(); final seconds = ((coordinate - degrees - minutes / 60) * 3600).floor(); diff --git a/lib/services/search_service.dart b/lib/services/search_service.dart index 580a40a8a..335864840 100644 --- a/lib/services/search_service.dart +++ b/lib/services/search_service.dart @@ -56,8 +56,10 @@ class SearchService { return _cachedFilesFuture!; } _logger.fine("Reading all files from db"); - _cachedFilesFuture = - FilesDB.instance.getAllFilesFromDB(ignoreCollections()); + _cachedFilesFuture = FilesDB.instance.getAllFilesFromDB( + ignoreCollections(), + dedupeByUploadId: true, + ); return _cachedFilesFuture!; } diff --git a/lib/ui/map/map_screen.dart b/lib/ui/map/map_screen.dart index 1a5c54c9b..df692a2fe 100644 --- a/lib/ui/map/map_screen.dart +++ b/lib/ui/map/map_screen.dart @@ -45,7 +45,7 @@ class _MapScreenState extends State { 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; diff --git a/lib/ui/map/map_view.dart b/lib/ui/map/map_view.dart index 471ca7098..f316a7719 100644 --- a/lib/ui/map/map_view.dart +++ b/lib/ui/map/map_view.dart @@ -77,8 +77,8 @@ class _MapViewState extends State { 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) { diff --git a/lib/ui/settings/backup/backup_section_widget.dart b/lib/ui/settings/backup/backup_section_widget.dart index ac57945fe..56c9f176e 100644 --- a/lib/ui/settings/backup/backup_section_widget.dart +++ b/lib/ui/settings/backup/backup_section_widget.dart @@ -1,3 +1,4 @@ +import "dart:async"; import 'dart:io'; import 'package:flutter/material.dart'; @@ -53,7 +54,7 @@ class BackupSectionWidgetState extends State { trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, onTap: () async { - routeToPage( + await routeToPage( context, BackupFolderSelectionPage( buttonText: S.of(context).backup, @@ -70,7 +71,7 @@ class BackupSectionWidgetState extends State { trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, onTap: () async { - routeToPage( + await routeToPage( context, const BackupSettingsScreen(), ); @@ -133,10 +134,12 @@ class BackupSectionWidgetState extends State { } if (duplicates.isEmpty) { - showErrorDialog( - context, - S.of(context).noDuplicates, - S.of(context).youveNoDuplicateFilesThatCanBeCleared, + unawaited( + showErrorDialog( + context, + S.of(context).noDuplicates, + S.of(context).youveNoDuplicateFilesThatCanBeCleared, + ), ); } else { final DeduplicationResult? result = @@ -167,16 +170,13 @@ class BackupSectionWidgetState extends State { S.of(context).youHaveSuccessfullyFreedUp(formatBytes(status.size)), firstButtonLabel: S.of(context).rateUs, firstButtonOnTap: () async { - UpdateService.instance.launchReviewUrl(); + await UpdateService.instance.launchReviewUrl(); }, firstButtonType: ButtonType.primary, secondButtonLabel: S.of(context).ok, secondButtonOnTap: () async { if (Platform.isIOS) { - showToast( - context, - S.of(context).remindToEmptyDeviceTrash, - ); + showToast(context, S.of(context).remindToEmptyDeviceTrash); } }, ); @@ -195,10 +195,7 @@ class BackupSectionWidgetState extends State { isInAlert: true, onTap: () async { if (Platform.isIOS) { - showToast( - context, - S.of(context).remindToEmptyDeviceTrash, - ); + showToast(context, S.of(context).remindToEmptyDeviceTrash); } }, ), diff --git a/lib/ui/tools/deduplicate_page.dart b/lib/ui/tools/deduplicate_page.dart index ae0045883..729d6b79b 100644 --- a/lib/ui/tools/deduplicate_page.dart +++ b/lib/ui/tools/deduplicate_page.dart @@ -1,3 +1,5 @@ +import "dart:developer"; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:photos/core/constants.dart'; @@ -8,14 +10,14 @@ import "package:photos/generated/l10n.dart"; import 'package:photos/models/duplicate_files.dart'; import 'package:photos/models/file/file.dart'; import 'package:photos/services/collections_service.dart'; -import 'package:photos/services/deduplication_service.dart'; +import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/viewer/file/detail_page.dart'; import 'package:photos/ui/viewer/file/thumbnail_widget.dart'; import 'package:photos/ui/viewer/gallery/empty_state.dart'; import 'package:photos/utils/data_util.dart'; import 'package:photos/utils/delete_file_util.dart'; +import "package:photos/utils/dialog_util.dart"; import 'package:photos/utils/navigation_util.dart'; -import 'package:photos/utils/toast_util.dart'; class DeduplicatePage extends StatefulWidget { final List duplicates; @@ -30,64 +32,37 @@ class _DeduplicatePageState extends State { static const crossAxisCount = 4; static const crossAxisSpacing = 4.0; static const headerRowCount = 3; - static final selectedOverlay = Container( - color: Colors.black.withOpacity(0.4), - child: const Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: EdgeInsets.only(right: 4, bottom: 4), - child: Icon( - Icons.check_circle, - size: 24, - color: Colors.white, - ), - ), - ), - ); - final Set _selectedFiles = {}; - final Map _fileSizeMap = {}; + final Set selectedGrids = {}; + late List _duplicates; - bool _shouldClubByCaptureTime = false; - bool _shouldClubByFileName = false; - bool toastShown = false; SortKey sortKey = SortKey.size; + late ValueNotifier _deleteProgress; @override void initState() { - _duplicates = DeduplicationService.instance.clubDuplicates( - widget.duplicates, - clubbingKey: (EnteFile f) => f.hash, - ); - _selectAllFilesButFirst(); - + _duplicates = widget.duplicates; + _deleteProgress = ValueNotifier(""); + _selectAllGrids(); super.initState(); } - void _selectAllFilesButFirst() { - _selectedFiles.clear(); - for (final duplicate in _duplicates) { - for (int index = 0; index < duplicate.files.length; index++) { - // Select all items but the first - if (index != 0) { - _selectedFiles.add(duplicate.files[index]); - } - // Maintain a map of fileID to fileSize for quick "space freed" computation - _fileSizeMap[duplicate.files[index].uploadedFileID] = duplicate.size; - } + @override + void dispose() { + _deleteProgress.dispose(); + super.dispose(); + } + + void _selectAllGrids() { + selectedGrids.clear(); + for (int idx = 0; idx < _duplicates.length; idx++) { + selectedGrids.add(idx); } } @override Widget build(BuildContext context) { - if (!toastShown) { - toastShown = true; - showShortToast( - context, - S.of(context).longpressOnAnItemToViewInFullscreen, - ); - } _sortDuplicates(); return Scaffold( appBar: AppBar( @@ -103,7 +78,7 @@ class _DeduplicatePageState extends State { ), onSelected: (dynamic value) { setState(() { - _selectedFiles.clear(); + selectedGrids.clear(); }); }, offset: const Offset(0, 50), @@ -141,15 +116,15 @@ class _DeduplicatePageState extends State { void _sortDuplicates() { _duplicates.sort((first, second) { - if (sortKey == SortKey.size) { - final aSize = first.files.length * first.size; - final bSize = second.files.length * second.size; - return bSize - aSize; - } else if (sortKey == SortKey.count) { - return second.files.length - first.files.length; - } else { - return second.files.first.creationTime! - - first.files.first.creationTime!; + switch (sortKey) { + case SortKey.size: + final aSize = first.files.length * first.size; + final bSize = second.files.length * second.size; + return bSize - aSize; + case SortKey.count: + return second.files.length - first.files.length; + default: + throw Exception("Unexpected sort key $sortKey"); } }); } @@ -188,10 +163,26 @@ class _DeduplicatePageState extends State { shrinkWrap: true, ), ), - _selectedFiles.isEmpty + selectedGrids.isEmpty ? const SizedBox.shrink() : Column( children: [ + ValueListenableBuilder( + valueListenable: _deleteProgress, + builder: (BuildContext context, value, Widget? child) { + if (value.isEmpty) { + return const SizedBox.shrink(); + } else { + return Padding( + padding: const EdgeInsets.all(4), + child: Text( + value, // Show the value + style: getEnteTextTheme(context).bodyMuted, + ), + ); + } + }, + ), _getDeleteButton(), const SizedBox(height: crossAxisSpacing / 2), ], @@ -200,89 +191,6 @@ class _DeduplicatePageState extends State { ); } - @Deprecated('Remove options for club by name, clean code in 2024') - Padding _getHeader() { - return Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S.of(context).reviewDeduplicateItems, - style: Theme.of(context).textTheme.titleSmall, - ), - const Padding( - padding: EdgeInsets.all(12), - ), - const Divider( - height: 0, - ), - ], - ), - ); - } - - @Deprecated('Remove options for clubbing, clean code in 2024') - Widget _getClubbingConfig() { - return Padding( - padding: const EdgeInsets.fromLTRB(12, 0, 12, 4), - child: Column( - children: [ - CheckboxListTile( - value: _shouldClubByFileName, - onChanged: (value) { - _shouldClubByFileName = value!; - if (_shouldClubByFileName) { - _shouldClubByCaptureTime = false; - } - _resetEntriesAndSelection(); - setState(() {}); - }, - title: Text(S.of(context).clubByFileName), - ), - CheckboxListTile( - value: _shouldClubByCaptureTime, - onChanged: (value) { - _shouldClubByCaptureTime = value!; - if (_shouldClubByCaptureTime) { - _shouldClubByFileName = false; - } - _resetEntriesAndSelection(); - setState(() {}); - }, - title: Text(S.of(context).clubByCaptureTime), - ), - const Padding( - padding: EdgeInsets.all(8), - ), - const Divider( - height: 0, - ), - const Padding( - padding: EdgeInsets.all(4), - ), - ], - ), - ); - } - - void _resetEntriesAndSelection() { - _duplicates = widget.duplicates; - late String? Function(EnteFile) clubbingKeyFn; - if (_shouldClubByCaptureTime) { - clubbingKeyFn = (EnteFile f) => f.creationTime?.toString() ?? ''; - } else if (_shouldClubByFileName) { - clubbingKeyFn = (EnteFile f) => f.displayName; - } else { - clubbingKeyFn = (EnteFile f) => f.hash; - } - _duplicates = DeduplicationService.instance.clubDuplicates( - _duplicates, - clubbingKey: clubbingKeyFn, - ); - _selectAllFilesButFirst(); - } - Widget _getSortMenu(BuildContext context) { Text sortOptionText(SortKey key) { String text = key.toString(); @@ -293,9 +201,6 @@ class _DeduplicatePageState extends State { case SortKey.size: text = S.of(context).totalSize; break; - case SortKey.time: - text = S.of(context).time; - break; } return Text( text, @@ -332,7 +237,15 @@ class _DeduplicatePageState extends State { ), onSelected: (int index) { setState(() { - sortKey = SortKey.values[index]; + final newKey = SortKey.values[index]; + if (newKey == sortKey) { + return; + } else { + sortKey = newKey; + if (selectedGrids.length != _duplicates.length) { + selectedGrids.clear(); + } + } }); }, itemBuilder: (context) { @@ -352,11 +265,16 @@ class _DeduplicatePageState extends State { } Widget _getDeleteButton() { - final String text = S.of(context).deleteItemCount(_selectedFiles.length); - int size = 0; - for (final file in _selectedFiles) { - size += _fileSizeMap[file.uploadedFileID]!; + int fileCount = 0; + int totalSize = 0; + for (int index = 0; index < _duplicates.length; index++) { + if (selectedGrids.contains(index)) { + final int toDeleteCount = _duplicates[index].files.length - 1; + fileCount += toDeleteCount; + totalSize += toDeleteCount * _duplicates[index].size; + } } + final String text = S.of(context).deleteItemCount(fileCount); return SizedBox( width: double.infinity, child: SafeArea( @@ -382,7 +300,7 @@ class _DeduplicatePageState extends State { ), const Padding(padding: EdgeInsets.all(2)), Text( - formatBytes(size), + formatBytes(totalSize), style: TextStyle( color: Theme.of(context) .colorScheme @@ -395,10 +313,12 @@ class _DeduplicatePageState extends State { ], ), onPressed: () async { - await deleteFilesFromRemoteOnly(context, _selectedFiles.toList()); - Bus.instance.fire(UserDetailsChangedEvent()); - Navigator.of(context) - .pop(DeduplicationResult(_selectedFiles.length, size)); + try { + await deleteDuplicates(totalSize); + } catch (e) { + log("Failed to delete duplicates", error: e); + showGenericErrorDialog(context: context, error: e).ignore(); + } }, ), ), @@ -406,18 +326,89 @@ class _DeduplicatePageState extends State { ); } + Future deleteDuplicates(int totalSize) async { + final List filesToDelele = []; + final Map> collectionToFilesToAddMap = {}; + for (int index = 0; index < _duplicates.length; index++) { + if (selectedGrids.contains(index)) { + final sortedFiles = _duplicates[index].sortByLocalIDs(); + final EnteFile fileToKeep = sortedFiles.first; + filesToDelele.addAll(sortedFiles.sublist(1)); + for (final collectionID in _duplicates[index].collectionIDs) { + if (fileToKeep.collectionID == collectionID) { + continue; + } + if (!collectionToFilesToAddMap.containsKey(collectionID)) { + collectionToFilesToAddMap[collectionID] = []; + } + collectionToFilesToAddMap[collectionID]!.add(fileToKeep); + } + } + } + final int collectionCnt = collectionToFilesToAddMap.keys.length; + int progress = 0; + for (final collectionID in collectionToFilesToAddMap.keys) { + if (!mounted) { + return; + } + if (collectionCnt > 0) { + progress++; + // calculate progress percentage upto 2 decimal places + final double percentage = (progress / collectionCnt) * 100; + _deleteProgress.value = '$percentage%'; + } + log("AddingNow ${collectionToFilesToAddMap[collectionID]!.length} files to $collectionID"); + await CollectionsService.instance.addSilentlyToCollection( + collectionID, + collectionToFilesToAddMap[collectionID]!, + ); + } + _deleteProgress.value = ""; + if (filesToDelele.isNotEmpty) { + await deleteFilesFromRemoteOnly(context, filesToDelele); + Bus.instance.fire(UserDetailsChangedEvent()); + Navigator.of(context) + .pop(DeduplicationResult(filesToDelele.length, totalSize)); + } + } + Widget _getGridView(DuplicateFiles duplicates, int itemIndex) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.fromLTRB(2, 4, 4, 12), - child: Text( - S.of(context).duplicateItemsGroup( - duplicates.files.length, - formatBytes(duplicates.size), + padding: const EdgeInsets.fromLTRB(2, 4, 2, 12), + child: GestureDetector( + onTap: () { + if (selectedGrids.contains(itemIndex)) { + selectedGrids.remove(itemIndex); + } else { + selectedGrids.add(itemIndex); + } + setState(() {}); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).duplicateItemsGroup( + duplicates.files.length, + formatBytes(duplicates.size), + ), + style: Theme.of(context).textTheme.titleSmall, ), - style: Theme.of(context).textTheme.titleSmall, + !selectedGrids.contains(itemIndex) + ? Icon( + Icons.check_circle_outlined, + color: getEnteColorScheme(context).strokeMuted, + size: 24, + ) + : const Icon( + Icons.check_circle, + size: 24, + ), + ], + ), ), ), Padding( @@ -445,12 +436,20 @@ class _DeduplicatePageState extends State { Widget _buildFile(BuildContext context, EnteFile file, int index) { return GestureDetector( onTap: () { - if (_selectedFiles.contains(file)) { - _selectedFiles.remove(file); - } else { - _selectedFiles.add(file); - } - setState(() {}); + final files = _duplicates[index].files; + routeToPage( + context, + DetailPage( + DetailPageConfiguration( + files, + null, + files.indexOf(file), + "deduplicate_", + mode: DetailPageMode.minimalistic, + ), + ), + forceCustomPageRoute: true, + ); }, onLongPress: () { HapticFeedback.lightImpact(); @@ -477,28 +476,18 @@ class _DeduplicatePageState extends State { height: (MediaQuery.of(context).size.width - (crossAxisSpacing * crossAxisCount)) / crossAxisCount, - child: Stack( - children: [ - Hero( - tag: "deduplicate_" + file.tag, - child: ClipRRect( - borderRadius: BorderRadius.circular(4), - child: ThumbnailWidget( - file, - diskLoadDeferDuration: thumbnailDiskLoadDeferDuration, - serverLoadDeferDuration: thumbnailServerLoadDeferDuration, - shouldShowLivePhotoOverlay: true, - key: Key("deduplicate_" + file.tag), - ), - ), + child: Hero( + tag: "deduplicate_" + file.tag, + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: ThumbnailWidget( + file, + diskLoadDeferDuration: thumbnailDiskLoadDeferDuration, + serverLoadDeferDuration: thumbnailServerLoadDeferDuration, + shouldShowLivePhotoOverlay: true, + key: Key("deduplicate_" + file.tag), ), - _selectedFiles.contains(file) - ? ClipRRect( - borderRadius: BorderRadius.circular(4), - child: selectedOverlay, - ) - : const SizedBox.shrink(), - ], + ), ), ), const SizedBox(height: 6), @@ -519,11 +508,7 @@ class _DeduplicatePageState extends State { } } -enum SortKey { - size, - count, - time, -} +enum SortKey { size, count } class DeduplicationResult { final int count; diff --git a/lib/ui/viewer/actions/file_selection_actions_widget.dart b/lib/ui/viewer/actions/file_selection_actions_widget.dart index 38524949e..dff39ef60 100644 --- a/lib/ui/viewer/actions/file_selection_actions_widget.dart +++ b/lib/ui/viewer/actions/file_selection_actions_widget.dart @@ -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, diff --git a/lib/ui/viewer/file/file_details_widget.dart b/lib/ui/viewer/file/file_details_widget.dart index 4ed1fe844..8c39cc927 100644 --- a/lib/ui/viewer/file/file_details_widget.dart +++ b/lib/ui/viewer/file/file_details_widget.dart @@ -86,6 +86,7 @@ class _FileDetailsWidgetState extends State { getExif(widget.file).then((exif) { _exifNotifier.value = exif; }); + super.initState(); } @@ -152,12 +153,52 @@ class _FileDetailsWidgetState extends State { ? 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 { 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 { diff --git a/lib/ui/viewer/file/video_widget_new.dart b/lib/ui/viewer/file/video_widget_new.dart index 6dcf57025..97659d7df 100644 --- a/lib/ui/viewer/file/video_widget_new.dart +++ b/lib/ui/viewer/file/video_widget_new.dart @@ -3,6 +3,7 @@ import "dart:io"; import "package:flutter/cupertino.dart"; import "package:flutter/material.dart"; +import "package:logging/logging.dart"; import "package:media_kit/media_kit.dart"; import "package:media_kit_video/media_kit_video.dart"; import "package:photos/core/constants.dart"; @@ -35,6 +36,7 @@ class VideoWidgetNew extends StatefulWidget { class _VideoWidgetNewState extends State with WidgetsBindingObserver { + final Logger _logger = Logger("VideoWidgetNew"); static const verticalMargin = 72.0; late final player = Player(); VideoController? controller; @@ -44,6 +46,9 @@ class _VideoWidgetNewState extends State @override void initState() { + _logger.info( + 'initState for ${widget.file.generatedID} with tag ${widget.file.tag} and name ${widget.file.displayName}', + ); super.initState(); WidgetsBinding.instance.addObserver(this); if (widget.file.isRemoteFile) { @@ -160,7 +165,7 @@ class _VideoWidgetNewState extends State getFileFromServer( widget.file, progressCallback: (count, total) { - if(!mounted) { + if (!mounted) { return; } _progressNotifier.value = count / (widget.file.fileSize ?? total); diff --git a/lib/ui/viewer/file/zoomable_live_image_new.dart b/lib/ui/viewer/file/zoomable_live_image_new.dart index 51ba8e41f..73a7f629b 100644 --- a/lib/ui/viewer/file/zoomable_live_image_new.dart +++ b/lib/ui/viewer/file/zoomable_live_image_new.dart @@ -33,7 +33,7 @@ class ZoomableLiveImageNew extends StatefulWidget { class _ZoomableLiveImageNewState extends State with SingleTickerProviderStateMixin { - final Logger _logger = Logger("ZoomableLiveImage"); + final Logger _logger = Logger("ZoomableLiveImageNew"); late EnteFile _enteFile; bool _showVideo = false; bool _isLoadingVideoPlayer = false; diff --git a/lib/ui/viewer/file_details/location_tags_widget.dart b/lib/ui/viewer/file_details/location_tags_widget.dart index b8b5ab78e..713e39ecc 100644 --- a/lib/ui/viewer/file_details/location_tags_widget.dart +++ b/lib/ui/viewer/file_details/location_tags_widget.dart @@ -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 createState() => _LocationTagsWidgetState(); @@ -58,13 +58,33 @@ class _LocationTagsWidgetState extends State { 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> _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 { hasChipButtons = false; onTap = () => showAddLocationSheet( context, - widget.centerPoint, + widget.file.location!, ); }); } @@ -112,7 +132,7 @@ class _LocationTagsWidgetState extends State { ChipButtonWidget( null, leadingIcon: Icons.add_outlined, - onTap: () => showAddLocationSheet(context, widget.centerPoint), + onTap: () => showAddLocationSheet(context, widget.file.location!), ), ); return result; diff --git a/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index c7953a7a5..de471597a 100644 --- a/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -395,9 +395,7 @@ class _GalleryAppBarWidgetState extends State { const Padding( padding: EdgeInsets.all(8), ), - Text( - "Clean Uncategorized", - ), + Text(S.of(context).cleanUncategorized), ], ), ), diff --git a/lib/ui/viewer/location/edit_center_point_tile_widget.dart b/lib/ui/viewer/location/edit_center_point_tile_widget.dart index 9891bd02e..f78009b87 100644 --- a/lib/ui/viewer/location/edit_center_point_tile_widget.dart +++ b/lib/ui/viewer/location/edit_center_point_tile_widget.dart @@ -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, ), ], diff --git a/lib/ui/viewer/location/update_location_data_widget.dart b/lib/ui/viewer/location/update_location_data_widget.dart new file mode 100644 index 000000000..780cf432e --- /dev/null +++ b/lib/ui/viewer/location/update_location_data_widget.dart @@ -0,0 +1,288 @@ +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 files; + const UpdateLocationDataWidget(this.files, {super.key}); + + @override + State createState() => + _UpdateLocationDataWidgetState(); +} + +class _UpdateLocationDataWidgetState extends State { + final MapController _mapController = MapController(); + ValueNotifier hasSelectedLocation = ValueNotifier(false); + final selectedLocation = ValueNotifier(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) { + 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 createState() => _UpdateLocationInfoState(); +} + +class _UpdateLocationInfoState extends State { + 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, + ), + ], + ); + }, + ); + } +} diff --git a/lib/ui/viewer/search/result/go_to_map_widget.dart b/lib/ui/viewer/search/result/go_to_map_widget.dart index fb449d824..29f970858 100644 --- a/lib/ui/viewer/search/result/go_to_map_widget.dart +++ b/lib/ui/viewer/search/result/go_to_map_widget.dart @@ -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"; diff --git a/lib/utils/exif_util.dart b/lib/utils/exif_util.dart index 7e8a99fbd..aa7ed4887 100644 --- a/lib/utils/exif_util.dart +++ b/lib/utils/exif_util.dart @@ -66,7 +66,7 @@ Future getCreationTimeFromEXIF( Location? locationFromExif(Map 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> readExifAsync(File file) async { ); } -GPSData _gpsDataFromExif(Map exif) { +GPSData gpsDataFromExif(Map exif) { final Map exifLocationData = { "lat": null, "long": null,