diff --git a/.vscode/launch.json.example b/.vscode/launch.json.example index 974de11cb61fb77633efa194c8af4a4b1d102eee..1e6b9ae6f6d06305642fa7f26497fc4df51a9e4a 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 66a4a1c2b9816719fcfd6c874de4f71f0a403139..c20f3a53cf57df92d430026b6a3479cca39d9e85 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 12f0c7881bde70beae41edde7f0543fb02832cca..2e7a760b5ebe64a518d4e6b98da531d4b3f3225c 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 a0b3b9f7d94395163d0ad789e17517d3b288b4a9..e91d3502e751e06a83f18ecfb9c12aaa989fce3e 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 9fd8013d17d36953d87a66290fecfe436b6f324e..3271c7968cc8b7ae3b9ae31775e8ac235c539825 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 4397e28d4155b393d21bc32ef6770cd1bb0f24b2..3eae25d3d9cd68ae576caf2b3358ad19892d0581 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 548fc33bcf1d776576281d3ccdc5171b3c634d5a..1fd4f860f80d4db95f0c3ab4cfd2c846aaba328e 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 50dbaf3f47d00b80a751a111a860b4438d0da336..2239a904f1437a543a29da82bcfefb7ddecc928f 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 761747c48aad11bc6c0980929ae2588e8ac172d6..8ecbf58b8467840ca8fe732ebd4707274dcce91a 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 7c52593426ef240c64a9e3236e08867e9047aae0..d9c3fcd9e26005d04dbf781390c2a3526e8dd93f 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 5d83fc64016a0e418de34c558876d5abf1c63341..cd612d97b350da3a8a63a212e9492d78f1d6172c 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 d162c4550838a19bb08aca7b9610b388dd653abf..97bb1d5ea306350981870579de29d8582407d411 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 f7cb0acffbc98f1df80e384ba4c4c6c60225ed86..7f3b6505f152f2e4387006d96e51a38408ba3831 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 bc38ae7284e19818ca152150f69c1594aa8cb0ed..1f8f26cc1bc9d129f268ce4e5e8b8888c5828da4 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 7ef02f16ba57730212e00455e83599d50a2e1ef4..fe8af370257cf01dbaa8c4e4c247284e6689988c 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 f861f8b5b801521c74a14c3324b4d39b1057f56b..5765cf70b417274ca877d8995919446f4266ae33 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 515bfae5aadc919c6bad61c489e8fc7345b8c4f3..6a71af50f22a01e2faae4682821447d5ddcc901d 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 086da19989e69f28dc5790b99bb12cb9a0aafe8a..5549ff9ef871f0c1e6af4b03b4d96c27eef0fa6e 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 d51611b2c4e83fd593624c50aed8361b1f7cf695..9bb80a8220676ec2f7b043a8f894bc6d4900901b 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 d7a9aabd85d1fb19e60bb225e23cac767371b332..0720f8d3ea6c1305b7a35d781ba8aae4dbf8aeb2 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 deab1cabe5e88b8f17dfefbafa1804cb6f1eef2d..fe9c4c64c8ac4d0fda685e2914e5d6d23d435cd6 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 d82d8e68245c6d93e19b5ec6851c3613af4e0660..7df3892f50e18539176a53c471618a2f3e06d192 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 515bfae5aadc919c6bad61c489e8fc7345b8c4f3..6a71af50f22a01e2faae4682821447d5ddcc901d 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 b4d60d451dd9bfa5bc98838e738438a095a8ce4d..bb8404dc45a07c8e6be4e24b401f7ebc29ee0b72 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 8106b8ca77db1fd4a93069e3fe1a3e537dffb902..d55787f17943ab664a3a1baf8475ab3387041c21 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 306b0a31cb7e1ae58ac8b9c470d6759d34d48591..7dfb7abc1c25b0420ebc7dc016ff3263df0fd522 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 cd674a3479b397ca6a22745b5dea522c737f5403..e0232c58ef9679bb9cad52101e2049f002692d9a 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 a445e3e8eeba198a303ad57d7a02fa70ae9d87df..7997edbbea7155cc853924550357908f8ee907a4 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 a0d363e075d6400dc3f30b52ad3356a79c54c3ac..8bb538859bd96dff13006341e6a18e09a3e7b8d2 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 a0481cd390766357b910fdb76fadbf8f5415fcb0..208cfa79015803f2fb8253a3ac097c1ff40f84cd 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 e3942345d448d28edae47accbd4906a204de21f9..ba0eb397f723d56f1e5125064aed508a0d395e0c 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 d565149fcb68ac74a2584027d6b39f90ef3676b4..e9485f50c737bc409f2e8546211a09fde4c5700c 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 ab6f7379c29ce94741e58220b3865a9c15415b3b..fc51ced3c733bac76bb1130c5fa481a46a2e5b6c 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 7af257c75fb68aa599e3f7af7d52b90a6327afb4..41ddff091222e99fd19acebe741e84f7028e3653 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; + } + if ((file.fileSize ?? 0) <= 0) { + file.fileSize = uploadIDToSize[file.uploadedFileID!] ?? 0; + } + if ((file.fileSize ?? 0) <= 0) { + continue; + } + filteredFiles.add(file); + } + + 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!); } - sizeToFilesMap[file.fileSize]!.add(file); } - final List dupesBySize = []; - for (final size in sizeToFilesMap.keys) { - final List files = sizeToFilesMap[size]!; + 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 d97b8a25e52899c6a55662624812d6bbb9af37fa..a72db91a9e83cd07e4ae6677fd26aba04b32f8ff 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 2bef2ef4195a626bcfd61de8aab559267768b6fd..10e3bfd727e1b9e6479e8ba4a7c9699a396caf94 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 580a40a8a1036fa431f694d9d1207096cb05d5dc..33586484068904615459a7274f26d341fe29be77 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 1a5c54c9bf4f641bceec5002cde90135934c98e0..df692a2fea17d85883fbaa77943b3e519b904f59 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 471ca70983fdde9842fed270a655aac3ea6afad7..f316a7719c58c40c63db111d3e88e412dc57d082 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 ac57945fe72cf7133e2a27de4023a35eca805278..56c9f176e9b1bc0b0f20b1f17ff8d1d0cc69a58a 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 ae0045883ed3207ec8877a4d2953a61d8207536c..729d6b79bb53d6607b8a2ef152ce7c9f3d96824b 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 38524949e7caa965a17d98ece499771dc985d36a..dff39ef60ab9c61ba87073918f4cdd4f5f4a4784 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 4ed1fe84429820ce91ecb87bbd9aede3513a709e..8c39cc92782b1155c1e8446e1504ab73738678bc 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 6dcf5702579a894c727babaff18b75f61fe662d7..97659d7dfd0e2b365735724d6af2838febd96899 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 51ba8e41f381ef6e48661b0a42e624dc34967dd1..73a7f629b72bed3b4df711f82b9e59258a7493da 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 b8b5ab78ede645fc2183effc1f6f394fb62c02a8..713e39ecc458998fda2b384e34ddde75cb3ecc69 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 c7953a7a5fb3bd4d801a349f41278cc196908d25..de471597a0b6b689d07e9722e3110ce3f07f74e8 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 9891bd02e2b0f5ca73639de1df3b82755e2a1032..f78009b87fbfbff3c17cef5d4489fd5c2dbfe148 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 0000000000000000000000000000000000000000..780cf432ebc4762def7afd5be2b8ab9bf1cb179e --- /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 fb449d824d748d8cfaceb6d65e524f6a5a76311c..29f97085855a18147b580791e2f0e0179942f33a 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 7e8a99fbdea8179893ca8663779332a4c7c30854..aa7ed488734a77c730d0926f0fe135800f17386e 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,