Merge branch 'main' into onnx
This commit is contained in:
commit
b6c8a79a78
51 changed files with 1210 additions and 381 deletions
12
.vscode/launch.json.example
vendored
12
.vscode/launch.json.example
vendored
|
@ -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"
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -1505,14 +1505,20 @@ class FilesDB {
|
|||
await batch.commit(noResult: true);
|
||||
}
|
||||
|
||||
Future<List<EnteFile>> getAllFilesFromDB(Set<int> collectionsToIgnore) async {
|
||||
Future<List<EnteFile>> getAllFilesFromDB(
|
||||
Set<int> collectionsToIgnore, {
|
||||
bool dedupeByUploadId = true,
|
||||
}) async {
|
||||
final db = await instance.database;
|
||||
final List<Map<String, dynamic>> result =
|
||||
await db.query(filesTable, orderBy: '$columnCreationTime DESC');
|
||||
final List<EnteFile> files = convertToFiles(result);
|
||||
final List<EnteFile> deduplicatedFiles = await applyDBFilters(
|
||||
files,
|
||||
DBFilterOptions(ignoredCollectionIDs: collectionsToIgnore),
|
||||
DBFilterOptions(
|
||||
ignoredCollectionIDs: collectionsToIgnore,
|
||||
dedupeUploadID: dedupeByUploadId,
|
||||
),
|
||||
);
|
||||
return deduplicatedFiles;
|
||||
}
|
||||
|
|
10
lib/generated/intl/messages_cs.dart
generated
10
lib/generated/intl/messages_cs.dart
generated
|
@ -24,15 +24,25 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"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")
|
||||
};
|
||||
}
|
||||
|
|
11
lib/generated/intl/messages_de.dart
generated
11
lib/generated/intl/messages_de.dart
generated
|
@ -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(
|
||||
|
|
17
lib/generated/intl/messages_en.dart
generated
17
lib/generated/intl/messages_en.dart
generated
|
@ -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(
|
||||
|
|
11
lib/generated/intl/messages_es.dart
generated
11
lib/generated/intl/messages_es.dart
generated
|
@ -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"),
|
||||
|
|
11
lib/generated/intl/messages_fr.dart
generated
11
lib/generated/intl/messages_fr.dart
generated
|
@ -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(
|
||||
|
|
11
lib/generated/intl/messages_it.dart
generated
11
lib/generated/intl/messages_it.dart
generated
|
@ -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(
|
||||
|
|
10
lib/generated/intl/messages_ko.dart
generated
10
lib/generated/intl/messages_ko.dart
generated
|
@ -24,15 +24,25 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"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")
|
||||
};
|
||||
}
|
||||
|
|
11
lib/generated/intl/messages_nl.dart
generated
11
lib/generated/intl/messages_nl.dart
generated
|
@ -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(
|
||||
|
|
10
lib/generated/intl/messages_no.dart
generated
10
lib/generated/intl/messages_no.dart
generated
|
@ -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")
|
||||
};
|
||||
|
|
10
lib/generated/intl/messages_pl.dart
generated
10
lib/generated/intl/messages_pl.dart
generated
|
@ -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"),
|
||||
|
|
10
lib/generated/intl/messages_pt.dart
generated
10
lib/generated/intl/messages_pt.dart
generated
|
@ -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"),
|
||||
|
|
16
lib/generated/intl/messages_zh.dart
generated
16
lib/generated/intl/messages_zh.dart
generated
|
@ -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("要重置您的密码,请先验证您的电子邮件。"),
|
||||
|
|
80
lib/generated/l10n.dart
generated
80
lib/generated/l10n.dart
generated
|
@ -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<S> {
|
||||
|
|
|
@ -5,5 +5,10 @@
|
|||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for",
|
||||
"contacts": "Contacts"
|
||||
"contacts": "Contacts",
|
||||
"editLocation": "Edit location",
|
||||
"selectALocation": "Select a location",
|
||||
"selectALocationFirst": "Select a location first",
|
||||
"changeLocationOfSelectedItems": "Change location of selected items?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
|
||||
}
|
|
@ -1158,5 +1158,10 @@
|
|||
"signOutFromOtherDevices": "Von anderen Geräten abmelden",
|
||||
"signOutOtherBody": "Falls du denkst, dass jemand dein Passwort kennen könnte, kannst du alle anderen Geräte von deinem Account abmelden.",
|
||||
"signOutOtherDevices": "Andere Geräte abmelden",
|
||||
"doNotSignOut": "Melde dich nicht ab"
|
||||
"doNotSignOut": "Melde dich nicht ab",
|
||||
"editLocation": "Edit location",
|
||||
"selectALocation": "Select a location",
|
||||
"selectALocationFirst": "Select a location first",
|
||||
"changeLocationOfSelectedItems": "Change location of selected items?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -968,5 +968,10 @@
|
|||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for",
|
||||
"contacts": "Contacts"
|
||||
"contacts": "Contacts",
|
||||
"editLocation": "Edit location",
|
||||
"selectALocation": "Select a location",
|
||||
"selectALocationFirst": "Select a location first",
|
||||
"changeLocationOfSelectedItems": "Change location of selected items?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
|
||||
}
|
|
@ -1149,5 +1149,10 @@
|
|||
"@addNew": {
|
||||
"description": "Text to add a new item (location tag, album, caption etc)"
|
||||
},
|
||||
"contacts": "Contacts"
|
||||
"contacts": "Contacts",
|
||||
"editLocation": "Edit location",
|
||||
"selectALocation": "Select a location",
|
||||
"selectALocationFirst": "Select a location first",
|
||||
"changeLocationOfSelectedItems": "Change location of selected items?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
|
||||
}
|
|
@ -1111,5 +1111,10 @@
|
|||
"addOnPageSubtitle": "Dettagli dei componenti aggiuntivi",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for",
|
||||
"contacts": "Contacts"
|
||||
"contacts": "Contacts",
|
||||
"editLocation": "Edit location",
|
||||
"selectALocation": "Select a location",
|
||||
"selectALocationFirst": "Select a location first",
|
||||
"changeLocationOfSelectedItems": "Change location of selected items?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
|
||||
}
|
|
@ -5,5 +5,10 @@
|
|||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for",
|
||||
"contacts": "Contacts"
|
||||
"contacts": "Contacts",
|
||||
"editLocation": "Edit location",
|
||||
"selectALocation": "Select a location",
|
||||
"selectALocationFirst": "Select a location first",
|
||||
"changeLocationOfSelectedItems": "Change location of selected items?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
|
||||
}
|
|
@ -1158,5 +1158,10 @@
|
|||
"signOutFromOtherDevices": "Log uit op andere apparaten",
|
||||
"signOutOtherBody": "Als je denkt dat iemand je wachtwoord zou kunnen kennen, kun je alle andere apparaten die je account gebruiken dwingen om uit te loggen.",
|
||||
"signOutOtherDevices": "Log uit op andere apparaten",
|
||||
"doNotSignOut": "Niet uitloggen"
|
||||
"doNotSignOut": "Niet uitloggen",
|
||||
"editLocation": "Edit location",
|
||||
"selectALocation": "Select a location",
|
||||
"selectALocationFirst": "Select a location first",
|
||||
"changeLocationOfSelectedItems": "Change location of selected items?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
|
||||
}
|
|
@ -19,5 +19,10 @@
|
|||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for",
|
||||
"contacts": "Contacts"
|
||||
"contacts": "Contacts",
|
||||
"editLocation": "Edit location",
|
||||
"selectALocation": "Select a location",
|
||||
"selectALocationFirst": "Select a location first",
|
||||
"changeLocationOfSelectedItems": "Change location of selected items?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
|
||||
}
|
|
@ -106,5 +106,10 @@
|
|||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for",
|
||||
"contacts": "Contacts"
|
||||
"contacts": "Contacts",
|
||||
"editLocation": "Edit location",
|
||||
"selectALocation": "Select a location",
|
||||
"selectALocationFirst": "Select a location first",
|
||||
"changeLocationOfSelectedItems": "Change location of selected items?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
|
||||
}
|
|
@ -272,5 +272,10 @@
|
|||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for",
|
||||
"contacts": "Contacts"
|
||||
"contacts": "Contacts",
|
||||
"editLocation": "Edit location",
|
||||
"selectALocation": "Select a location",
|
||||
"selectALocationFirst": "Select a location first",
|
||||
"changeLocationOfSelectedItems": "Change location of selected items?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
|
||||
}
|
|
@ -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": "清除未分类的"
|
||||
}
|
|
@ -4,38 +4,48 @@ import 'package:photos/models/file/file.dart';
|
|||
import 'package:photos/services/collections_service.dart';
|
||||
|
||||
class DuplicateFilesResponse {
|
||||
final List<DuplicateItems> duplicates;
|
||||
DuplicateFilesResponse(this.duplicates);
|
||||
final List<FileWithSameSize> sameSizeFiles;
|
||||
DuplicateFilesResponse(this.sameSizeFiles);
|
||||
|
||||
factory DuplicateFilesResponse.fromMap(Map<String, dynamic> map) {
|
||||
return DuplicateFilesResponse(
|
||||
List<DuplicateItems>.from(
|
||||
map['duplicates']?.map((x) => DuplicateItems.fromMap(x)),
|
||||
List<FileWithSameSize>.from(
|
||||
map['duplicates']?.map((x) => FileWithSameSize.fromMap(x)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<int, int> toUploadIDToSize() {
|
||||
final Map<int, int> 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<int> fileIDs;
|
||||
final int size;
|
||||
DuplicateItems(this.fileIDs, this.size);
|
||||
FileWithSameSize(this.fileIDs, this.size);
|
||||
|
||||
factory DuplicateItems.fromMap(Map<String, dynamic> map) {
|
||||
return DuplicateItems(
|
||||
factory FileWithSameSize.fromMap(Map<String, dynamic> map) {
|
||||
return FileWithSameSize(
|
||||
List<int>.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<EnteFile> files;
|
||||
final int size;
|
||||
final Set<int> 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<EnteFile> sortByLocalIDs() {
|
||||
final List<EnteFile> filesWithoutLocalID = [];
|
||||
final List<EnteFile> 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)';
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'package:photos/utils/date_time_util.dart';
|
|||
import 'package:photos/utils/exif_util.dart';
|
||||
import 'package:photos/utils/file_uploader_util.dart';
|
||||
|
||||
//Todo: files with no location data have lat and long set to 0.0. This should ideally be null.
|
||||
class EnteFile {
|
||||
int? generatedID;
|
||||
int? uploadedFileID;
|
||||
|
@ -271,6 +272,7 @@ class EnteFile {
|
|||
int get width {
|
||||
return pubMagicMetadata?.w ?? 0;
|
||||
}
|
||||
|
||||
bool get hasDimensions {
|
||||
return height != 0 && width != 0;
|
||||
}
|
||||
|
|
|
@ -242,6 +242,10 @@ extension GalleyTypeExtension on GalleryType {
|
|||
bool showRemoveFromHiddenAlbum() {
|
||||
return this == GalleryType.hiddenOwnedCollection;
|
||||
}
|
||||
|
||||
bool showEditLocation() {
|
||||
return this != GalleryType.sharedCollection;
|
||||
}
|
||||
}
|
||||
|
||||
extension GalleryAppBarExtn on GalleryType {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -310,6 +310,21 @@ class CollectionsService {
|
|||
.toList();
|
||||
}
|
||||
|
||||
// getActiveCollections returns list of collections which are not deleted yet
|
||||
Set<int> 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<Collection> 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<void> addSilentlyToCollection(
|
||||
int collectionID,
|
||||
List<EnteFile> 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 = <String, dynamic>{};
|
||||
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<EnteFile> linkLocalFileToExistingUploadedFileInAnotherCollection(
|
||||
int destCollectionID, {
|
||||
required EnteFile localFileToUpload,
|
||||
|
|
|
@ -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<List<DuplicateFiles>> getDuplicateFiles() async {
|
||||
try {
|
||||
final bool hasFileSizes = await FilesService.instance.hasMigratedSizes();
|
||||
if (hasFileSizes) {
|
||||
final List<DuplicateFiles> result = await _getDuplicateFilesFromLocal();
|
||||
return result;
|
||||
}
|
||||
final DuplicateFilesResponse dupes = await _fetchDuplicateFileIDs();
|
||||
final ids = <int>[];
|
||||
for (final dupe in dupes.duplicates) {
|
||||
ids.addAll(dupe.fileIDs);
|
||||
}
|
||||
final fileMap = await FilesDB.instance.getFilesFromIDs(ids);
|
||||
final result = <DuplicateFiles>[];
|
||||
final missingFileIDs = <int>[];
|
||||
for (final dupe in dupes.duplicates) {
|
||||
final files = <EnteFile>[];
|
||||
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<DuplicateFiles> result = await _getDuplicateFiles();
|
||||
return result;
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to get dedupeFile", e, s);
|
||||
|
@ -74,61 +26,65 @@ class DeduplicationService {
|
|||
}
|
||||
}
|
||||
|
||||
List<DuplicateFiles> clubDuplicates(
|
||||
List<DuplicateFiles> dupesBySize, {
|
||||
required String? Function(EnteFile) clubbingKey,
|
||||
}) {
|
||||
final dupesBySizeAndClubKey = <DuplicateFiles>[];
|
||||
for (final sizeBasedDupe in dupesBySize) {
|
||||
final Map<String, List<EnteFile>> clubKeyToFilesMap = {};
|
||||
for (final file in sizeBasedDupe.files) {
|
||||
final String? clubKey = clubbingKey(file);
|
||||
if (clubKey == null || clubKey.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
if (!clubKeyToFilesMap.containsKey(clubKey)) {
|
||||
clubKeyToFilesMap[clubKey] = <EnteFile>[];
|
||||
}
|
||||
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<List<DuplicateFiles>> _getDuplicateFiles() async {
|
||||
Map<int, int> uploadIDToSize = {};
|
||||
final bool hasFileSizes = await FilesService.instance.hasMigratedSizes();
|
||||
if (!hasFileSizes) {
|
||||
final DuplicateFilesResponse dupes = await _fetchDuplicateFileIDs();
|
||||
uploadIDToSize = dupes.toUploadIDToSize();
|
||||
}
|
||||
return dupesBySizeAndClubKey;
|
||||
}
|
||||
final Set<int> allowedCollectionIDs =
|
||||
CollectionsService.instance.nonHiddenOwnedCollections();
|
||||
|
||||
Future<List<DuplicateFiles>> _getDuplicateFilesFromLocal() async {
|
||||
final List<EnteFile> 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<int, List<EnteFile>> sizeToFilesMap = {};
|
||||
final List<EnteFile> filteredFiles = [];
|
||||
for (final file in allFiles) {
|
||||
if (!sizeToFilesMap.containsKey(file.fileSize)) {
|
||||
sizeToFilesMap[file.fileSize!] = <EnteFile>[];
|
||||
if (!file.isUploaded ||
|
||||
(file.hash ?? '').isEmpty ||
|
||||
(file.ownerID ?? 0) != ownerID ||
|
||||
(!allowedCollectionIDs.contains(file.collectionID!))) {
|
||||
continue;
|
||||
}
|
||||
sizeToFilesMap[file.fileSize]!.add(file);
|
||||
if ((file.fileSize ?? 0) <= 0) {
|
||||
file.fileSize = uploadIDToSize[file.uploadedFileID!] ?? 0;
|
||||
}
|
||||
if ((file.fileSize ?? 0) <= 0) {
|
||||
continue;
|
||||
}
|
||||
filteredFiles.add(file);
|
||||
}
|
||||
final List<DuplicateFiles> dupesBySize = [];
|
||||
for (final size in sizeToFilesMap.keys) {
|
||||
final List<EnteFile> files = sizeToFilesMap[size]!;
|
||||
|
||||
final Map<String, List<EnteFile>> sizeHashToFilesMap = {};
|
||||
final Map<String, Set<int>> sizeHashToCollectionsSet = {};
|
||||
final Set<int> processedFileIds = <int>{};
|
||||
for (final file in filteredFiles) {
|
||||
final key = '${file.fileSize}-${file.hash}';
|
||||
if (!sizeHashToFilesMap.containsKey(key)) {
|
||||
sizeHashToFilesMap[key] = <EnteFile>[];
|
||||
sizeHashToCollectionsSet[key] = <int>{};
|
||||
}
|
||||
sizeHashToCollectionsSet[key]!.add(file.collectionID!);
|
||||
if (!processedFileIds.contains(file.uploadedFileID)) {
|
||||
sizeHashToFilesMap[key]!.add(file);
|
||||
processedFileIds.add(file.uploadedFileID!);
|
||||
}
|
||||
}
|
||||
final List<DuplicateFiles> dupesBySizeHash = [];
|
||||
for (final key in sizeHashToFilesMap.keys) {
|
||||
final List<EnteFile> files = sizeHashToFilesMap[key]!;
|
||||
final Set<int> 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<DuplicateFilesResponse> _fetchDuplicateFileIDs() async {
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import "package:latlong2/latlong.dart";
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/network/network.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/extensions/list.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import "package:photos/models/file_load_result.dart";
|
||||
import "package:photos/models/metadata/file_magic.dart";
|
||||
import 'package:photos/services/file_magic_service.dart';
|
||||
import "package:photos/services/ignored_files_service.dart";
|
||||
import "package:photos/ui/components/action_sheet_widget.dart";
|
||||
import "package:photos/ui/components/buttons/button_widget.dart";
|
||||
import "package:photos/ui/components/models/button_type.dart";
|
||||
import 'package:photos/utils/date_time_util.dart';
|
||||
|
||||
class FilesService {
|
||||
|
@ -85,6 +91,79 @@ class FilesService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> bulkEditLocationData(
|
||||
List<EnteFile> files,
|
||||
LatLng location,
|
||||
BuildContext context,
|
||||
) async {
|
||||
final List<EnteFile> uploadedFiles =
|
||||
files.where((element) => element.uploadedFileID != null).toList();
|
||||
|
||||
final List<EnteFile> remoteFilesToUpdate = [];
|
||||
final Map<int, Map<String, dynamic>> fileIDToUpdateMetadata = {};
|
||||
await showActionSheet(
|
||||
context: context,
|
||||
body: S.of(context).changeLocationOfSelectedItems,
|
||||
buttons: [
|
||||
ButtonWidget(
|
||||
labelText: S.of(context).yes,
|
||||
buttonType: ButtonType.neutral,
|
||||
buttonSize: ButtonSize.large,
|
||||
shouldStickToDarkTheme: true,
|
||||
buttonAction: ButtonAction.first,
|
||||
shouldSurfaceExecutionStates: true,
|
||||
isInAlert: true,
|
||||
onTap: () async {
|
||||
await _editLocationData(
|
||||
uploadedFiles,
|
||||
fileIDToUpdateMetadata,
|
||||
remoteFilesToUpdate,
|
||||
location,
|
||||
);
|
||||
},
|
||||
),
|
||||
ButtonWidget(
|
||||
labelText: S.of(context).cancel,
|
||||
buttonType: ButtonType.secondary,
|
||||
buttonSize: ButtonSize.large,
|
||||
shouldStickToDarkTheme: true,
|
||||
buttonAction: ButtonAction.cancel,
|
||||
isInAlert: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _editLocationData(
|
||||
List<EnteFile> uploadedFiles,
|
||||
Map<int, Map<String, dynamic>> fileIDToUpdateMetadata,
|
||||
List<EnteFile> remoteFilesToUpdate,
|
||||
LatLng location,
|
||||
) async {
|
||||
for (EnteFile remoteFile in uploadedFiles) {
|
||||
// discard files not owned by user and also dedupe already processed
|
||||
// files
|
||||
if (remoteFile.ownerID != _config.getUserID()! ||
|
||||
fileIDToUpdateMetadata.containsKey(remoteFile.uploadedFileID!)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
remoteFilesToUpdate.add(remoteFile);
|
||||
fileIDToUpdateMetadata[remoteFile.uploadedFileID!] = {
|
||||
latKey: location.latitude,
|
||||
longKey: location.longitude,
|
||||
};
|
||||
}
|
||||
|
||||
if (remoteFilesToUpdate.isNotEmpty) {
|
||||
await FileMagicService.instance.updatePublicMagicMetadata(
|
||||
remoteFilesToUpdate,
|
||||
null,
|
||||
metadataUpdateMap: fileIDToUpdateMetadata,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: this method is not used anywhere, but it is kept for future
|
||||
// reference when we add bulk EditTime feature
|
||||
Future<void> bulkEditTime(
|
||||
|
|
|
@ -114,17 +114,25 @@ class LocationService {
|
|||
return false;
|
||||
}
|
||||
|
||||
String convertLocationToDMS(Location centerPoint) {
|
||||
/// returns [lat, lng]
|
||||
List<String>? convertLocationToDMS(Location centerPoint) {
|
||||
if (centerPoint.latitude == null || centerPoint.longitude == null) {
|
||||
return null;
|
||||
}
|
||||
final lat = centerPoint.latitude!;
|
||||
final long = centerPoint.longitude!;
|
||||
final latRef = lat >= 0 ? "N" : "S";
|
||||
final longRef = long >= 0 ? "E" : "W";
|
||||
final latDMS = convertCoordinateToDMS(lat.abs());
|
||||
final longDMS = convertCoordinateToDMS(long.abs());
|
||||
return "${latDMS[0]}°${latDMS[1]}'${latDMS[2]}\"$latRef, ${longDMS[0]}°${longDMS[1]}'${longDMS[2]}\"$longRef";
|
||||
final latDMS = _convertCoordinateToDMS(lat.abs());
|
||||
final longDMS = _convertCoordinateToDMS(long.abs());
|
||||
|
||||
return [
|
||||
"${latDMS[0]}°${latDMS[1]}'${latDMS[2]}\" $latRef",
|
||||
"${longDMS[0]}°${longDMS[1]}'${longDMS[2]}\" $longRef",
|
||||
];
|
||||
}
|
||||
|
||||
List<int> convertCoordinateToDMS(double coordinate) {
|
||||
List<int> _convertCoordinateToDMS(double coordinate) {
|
||||
final degrees = coordinate.floor();
|
||||
final minutes = ((coordinate - degrees) * 60).floor();
|
||||
final seconds = ((coordinate - degrees - minutes / 60) * 3600).floor();
|
||||
|
|
|
@ -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!;
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||
double maxZoom = 18.0;
|
||||
double minZoom = 2.8;
|
||||
int debounceDuration = 500;
|
||||
LatLng center = LatLng(46.7286, 4.8614);
|
||||
LatLng center = const LatLng(46.7286, 4.8614);
|
||||
final Logger _logger = Logger("_MapScreenState");
|
||||
StreamSubscription? _mapMoveSubscription;
|
||||
Isolate? isolate;
|
||||
|
|
|
@ -77,8 +77,8 @@ class _MapViewState extends State<MapView> {
|
|||
enableMultiFingerGestureRace: true,
|
||||
zoom: widget.initialZoom,
|
||||
maxBounds: LatLngBounds(
|
||||
LatLng(-90, -180),
|
||||
LatLng(90, 180),
|
||||
const LatLng(-90, -180),
|
||||
const LatLng(90, 180),
|
||||
),
|
||||
onPositionChanged: (position, hasGesture) {
|
||||
if (position.bounds != null) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import "dart:async";
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -53,7 +54,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
|
|||
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<BackupSectionWidget> {
|
|||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
routeToPage(
|
||||
await routeToPage(
|
||||
context,
|
||||
const BackupSettingsScreen(),
|
||||
);
|
||||
|
@ -133,10 +134,12 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
|
|||
}
|
||||
|
||||
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<BackupSectionWidget> {
|
|||
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<BackupSectionWidget> {
|
|||
isInAlert: true,
|
||||
onTap: () async {
|
||||
if (Platform.isIOS) {
|
||||
showToast(
|
||||
context,
|
||||
S.of(context).remindToEmptyDeviceTrash,
|
||||
);
|
||||
showToast(context, S.of(context).remindToEmptyDeviceTrash);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
|
@ -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<DuplicateFiles> duplicates;
|
||||
|
@ -30,64 +32,37 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
|
|||
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<EnteFile> _selectedFiles = <EnteFile>{};
|
||||
final Map<int?, int> _fileSizeMap = {};
|
||||
final Set<int> selectedGrids = <int>{};
|
||||
|
||||
late List<DuplicateFiles> _duplicates;
|
||||
bool _shouldClubByCaptureTime = false;
|
||||
bool _shouldClubByFileName = false;
|
||||
bool toastShown = false;
|
||||
|
||||
SortKey sortKey = SortKey.size;
|
||||
late ValueNotifier<String> _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<DeduplicatePage> {
|
|||
),
|
||||
onSelected: (dynamic value) {
|
||||
setState(() {
|
||||
_selectedFiles.clear();
|
||||
selectedGrids.clear();
|
||||
});
|
||||
},
|
||||
offset: const Offset(0, 50),
|
||||
|
@ -141,15 +116,15 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
|
|||
|
||||
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<DeduplicatePage> {
|
|||
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<DeduplicatePage> {
|
|||
);
|
||||
}
|
||||
|
||||
@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<DeduplicatePage> {
|
|||
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<DeduplicatePage> {
|
|||
),
|
||||
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<DeduplicatePage> {
|
|||
}
|
||||
|
||||
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<DeduplicatePage> {
|
|||
),
|
||||
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<DeduplicatePage> {
|
|||
],
|
||||
),
|
||||
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<DeduplicatePage> {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> deleteDuplicates(int totalSize) async {
|
||||
final List<EnteFile> filesToDelele = [];
|
||||
final Map<int, List<EnteFile>> 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<DeduplicatePage> {
|
|||
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<DeduplicatePage> {
|
|||
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<DeduplicatePage> {
|
|||
}
|
||||
}
|
||||
|
||||
enum SortKey {
|
||||
size,
|
||||
count,
|
||||
time,
|
||||
}
|
||||
enum SortKey { size, count }
|
||||
|
||||
class DeduplicationResult {
|
||||
final int count;
|
||||
|
|
|
@ -3,6 +3,7 @@ import "dart:async";
|
|||
import 'package:fast_base58/fast_base58.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/collection/collection.dart';
|
||||
|
@ -15,6 +16,8 @@ import "package:photos/models/metadata/common_keys.dart";
|
|||
import 'package:photos/models/selected_files.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/services/hidden_service.dart';
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import 'package:photos/ui/actions/collection/collection_file_actions.dart';
|
||||
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
|
||||
import 'package:photos/ui/collections/collection_action_sheet.dart';
|
||||
|
@ -24,6 +27,7 @@ import 'package:photos/ui/components/buttons/button_widget.dart';
|
|||
import 'package:photos/ui/components/models/button_type.dart';
|
||||
import 'package:photos/ui/sharing/manage_links_widget.dart';
|
||||
import "package:photos/ui/tools/collage/collage_creator_page.dart";
|
||||
import "package:photos/ui/viewer/location/update_location_data_widget.dart";
|
||||
import 'package:photos/utils/delete_file_util.dart';
|
||||
import 'package:photos/utils/magic_util.dart';
|
||||
import 'package:photos/utils/navigation_util.dart';
|
||||
|
@ -308,6 +312,56 @@ class _FileSelectionActionsWidgetState
|
|||
);
|
||||
}
|
||||
|
||||
if (widget.type.showEditLocation()) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
shouldShow: widget.selectedFiles.files.any(
|
||||
(element) => (element.ownerID == currentUserID),
|
||||
),
|
||||
labelText: S.of(context).editLocation,
|
||||
icon: Icons.edit_location_alt_outlined,
|
||||
onTap: () async {
|
||||
await showBarModalBottomSheet(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(5),
|
||||
),
|
||||
),
|
||||
backgroundColor: getEnteColorScheme(context).backgroundElevated,
|
||||
barrierColor: backdropFaintDark,
|
||||
topControl: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
// This container is for increasing the tap area
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 36,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
Container(
|
||||
height: 5,
|
||||
width: 40,
|
||||
decoration: const BoxDecoration(
|
||||
color: backgroundElevated2Light,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return UpdateLocationDataWidget(
|
||||
widget.selectedFiles.files.toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
labelText: S.of(context).share,
|
||||
|
|
|
@ -86,6 +86,7 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
|||
getExif(widget.file).then((exif) {
|
||||
_exifNotifier.value = exif;
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -152,12 +153,52 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
|||
? Column(
|
||||
children: [
|
||||
LocationTagsWidget(
|
||||
widget.file.location!,
|
||||
widget.file,
|
||||
),
|
||||
const FileDetailsDivider(),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
|
||||
///To be used when state issues are fixed when location is updated.
|
||||
//
|
||||
// file.fileType != FileType.video &&
|
||||
// file.ownerID == _currentUserID
|
||||
// ? Column(
|
||||
// children: [
|
||||
// InfoItemWidget(
|
||||
// leadingIcon: Icons.pin_drop_outlined,
|
||||
// title: "No location data",
|
||||
// subtitleSection: Future.value(
|
||||
// [
|
||||
// Text(
|
||||
// "Add location data",
|
||||
// style: getEnteTextTheme(context).miniBoldMuted,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// hasChipButtons: false,
|
||||
// onTap: () async {
|
||||
// await showBarModalBottomSheet(
|
||||
// shape: const RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.vertical(
|
||||
// top: Radius.circular(5),
|
||||
// ),
|
||||
// ),
|
||||
// backgroundColor: getEnteColorScheme(context)
|
||||
// .backgroundElevated,
|
||||
// barrierColor: backdropFaintDark,
|
||||
// context: context,
|
||||
// builder: (context) {
|
||||
// return UpdateLocationDataWidget([file]);
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// const FileDetailsDivider(),
|
||||
// ],
|
||||
// )
|
||||
// : const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
@ -280,7 +321,8 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
|||
if (imageWidth != null && imageLength != null) {
|
||||
_exifData["resolution"] = '$imageWidth x $imageLength';
|
||||
final double megaPixels =
|
||||
(imageWidth.values.firstAsInt() * imageLength.values.firstAsInt()) / 1000000;
|
||||
(imageWidth.values.firstAsInt() * imageLength.values.firstAsInt()) /
|
||||
1000000;
|
||||
final double roundedMegaPixels = (megaPixels * 10).round() / 10.0;
|
||||
_exifData['megaPixels'] = roundedMegaPixels..toStringAsFixed(1);
|
||||
} else {
|
||||
|
|
|
@ -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<VideoWidgetNew>
|
||||
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<VideoWidgetNew>
|
|||
|
||||
@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<VideoWidgetNew>
|
|||
getFileFromServer(
|
||||
widget.file,
|
||||
progressCallback: (count, total) {
|
||||
if(!mounted) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
_progressNotifier.value = count / (widget.file.fileSize ?? total);
|
||||
|
|
|
@ -33,7 +33,7 @@ class ZoomableLiveImageNew extends StatefulWidget {
|
|||
|
||||
class _ZoomableLiveImageNewState extends State<ZoomableLiveImageNew>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final Logger _logger = Logger("ZoomableLiveImage");
|
||||
final Logger _logger = Logger("ZoomableLiveImageNew");
|
||||
late EnteFile _enteFile;
|
||||
bool _showVideo = false;
|
||||
bool _isLoadingVideoPlayer = false;
|
||||
|
|
|
@ -4,7 +4,7 @@ import "package:flutter/material.dart";
|
|||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/location_tag_updated_event.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/models/location/location.dart";
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/services/location_service.dart";
|
||||
import "package:photos/states/location_screen_state.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
|
@ -15,8 +15,8 @@ import "package:photos/ui/viewer/location/location_screen.dart";
|
|||
import "package:photos/utils/navigation_util.dart";
|
||||
|
||||
class LocationTagsWidget extends StatefulWidget {
|
||||
final Location centerPoint;
|
||||
const LocationTagsWidget(this.centerPoint, {super.key});
|
||||
final EnteFile file;
|
||||
const LocationTagsWidget(this.file, {super.key});
|
||||
|
||||
@override
|
||||
State<LocationTagsWidget> createState() => _LocationTagsWidgetState();
|
||||
|
@ -58,13 +58,33 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
|
|||
subtitleSection: locationTagChips,
|
||||
hasChipButtons: hasChipButtons ?? true,
|
||||
onTap: onTap,
|
||||
|
||||
/// to be used when state issues are fixed when location is updated
|
||||
// editOnTap: widget.file.ownerID == Configuration.instance.getUserID()!
|
||||
// ? () {
|
||||
// showBarModalBottomSheet(
|
||||
// shape: const RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.vertical(
|
||||
// top: Radius.circular(5),
|
||||
// ),
|
||||
// ),
|
||||
// backgroundColor:
|
||||
// getEnteColorScheme(context).backgroundElevated,
|
||||
// barrierColor: backdropFaintDark,
|
||||
// context: context,
|
||||
// builder: (context) {
|
||||
// return UpdateLocationDataWidget([widget.file]);
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Widget>> _getLocationTags() async {
|
||||
final locationTags = await LocationService.instance
|
||||
.enclosingLocationTags(widget.centerPoint);
|
||||
.enclosingLocationTags(widget.file.location!);
|
||||
if (locationTags.isEmpty) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
|
@ -73,7 +93,7 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
|
|||
hasChipButtons = false;
|
||||
onTap = () => showAddLocationSheet(
|
||||
context,
|
||||
widget.centerPoint,
|
||||
widget.file.location!,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -112,7 +132,7 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
|
|||
ChipButtonWidget(
|
||||
null,
|
||||
leadingIcon: Icons.add_outlined,
|
||||
onTap: () => showAddLocationSheet(context, widget.centerPoint),
|
||||
onTap: () => showAddLocationSheet(context, widget.file.location!),
|
||||
),
|
||||
);
|
||||
return result;
|
||||
|
|
|
@ -395,9 +395,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text(
|
||||
"Clean Uncategorized",
|
||||
),
|
||||
Text(S.of(context).cleanUncategorized),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
|
|
288
lib/ui/viewer/location/update_location_data_widget.dart
Normal file
288
lib/ui/viewer/location/update_location_data_widget.dart
Normal file
|
@ -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<EnteFile> files;
|
||||
const UpdateLocationDataWidget(this.files, {super.key});
|
||||
|
||||
@override
|
||||
State<UpdateLocationDataWidget> createState() =>
|
||||
_UpdateLocationDataWidgetState();
|
||||
}
|
||||
|
||||
class _UpdateLocationDataWidgetState extends State<UpdateLocationDataWidget> {
|
||||
final MapController _mapController = MapController();
|
||||
ValueNotifier hasSelectedLocation = ValueNotifier(false);
|
||||
final selectedLocation = ValueNotifier<LatLng?>(null);
|
||||
final isDragging = ValueNotifier(false);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
hasSelectedLocation.dispose();
|
||||
selectedLocation.dispose();
|
||||
_mapController.dispose();
|
||||
isDragging.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Logger("UpdateLocationDataWiget").info("building");
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
return Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
FlutterMap(
|
||||
mapController: _mapController,
|
||||
options: MapOptions(
|
||||
enableMultiFingerGestureRace: true,
|
||||
zoom: 3,
|
||||
maxZoom: 18.0,
|
||||
minZoom: 2.8,
|
||||
onMapEvent: (p0) {
|
||||
if (p0.source == MapEventSource.onDrag) {
|
||||
isDragging.value = true;
|
||||
} else if (p0.source == MapEventSource.dragEnd) {
|
||||
isDragging.value = false;
|
||||
}
|
||||
},
|
||||
onTap: (tapPosition, latlng) {
|
||||
final zoom = selectedLocation.value == null
|
||||
? _mapController.zoom + 2.0
|
||||
: _mapController.zoom;
|
||||
_mapController.move(latlng, zoom);
|
||||
|
||||
selectedLocation.value = latlng;
|
||||
hasSelectedLocation.value = true;
|
||||
},
|
||||
onPositionChanged: (position, hasGesture) {
|
||||
if (selectedLocation.value != null) {
|
||||
selectedLocation.value = position.center;
|
||||
}
|
||||
},
|
||||
),
|
||||
nonRotatedChildren: const [
|
||||
OSMFranceTileAttributes(),
|
||||
],
|
||||
children: const [
|
||||
OSMFranceTileLayer(),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
top: 20,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: getEnteColorScheme(context).backgroundElevated,
|
||||
boxShadow: shadowFloatFaintLight,
|
||||
),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: selectedLocation,
|
||||
builder: (context, value, _) {
|
||||
final locationInDMS =
|
||||
LocationService.instance.convertLocationToDMS(
|
||||
Location(
|
||||
latitude: value?.latitude,
|
||||
longitude: value?.longitude,
|
||||
),
|
||||
);
|
||||
return locationInDMS != null
|
||||
? ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: 80 * MediaQuery.textScaleFactorOf(context),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
locationInDMS[0],
|
||||
style: textTheme.mini,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
locationInDMS[1],
|
||||
style: textTheme.mini,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const UpdateLocationInfo();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 48,
|
||||
right: 24,
|
||||
left: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
MapButton(
|
||||
icon: Icons.add,
|
||||
onPressed: () {
|
||||
_mapController.move(
|
||||
_mapController.center,
|
||||
_mapController.zoom + 1,
|
||||
);
|
||||
},
|
||||
heroTag: 'zoom-in',
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
MapButton(
|
||||
icon: Icons.remove,
|
||||
onPressed: () {
|
||||
_mapController.move(
|
||||
_mapController.center,
|
||||
_mapController.zoom - 1,
|
||||
);
|
||||
},
|
||||
heroTag: 'zoom-out',
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
MapButton(
|
||||
icon: Icons.check,
|
||||
onPressed: () async {
|
||||
if (selectedLocation.value == null) {
|
||||
showShortToast(
|
||||
context,
|
||||
S.of(context).selectALocationFirst,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await FilesService.instance.bulkEditLocationData(
|
||||
widget.files,
|
||||
selectedLocation.value!,
|
||||
context,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
heroTag: 'add-location',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: hasSelectedLocation,
|
||||
builder: (context, value, _) {
|
||||
return value
|
||||
? Positioned(
|
||||
bottom: 32,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ValueListenableBuilder(
|
||||
valueListenable: isDragging,
|
||||
builder: (context, value, child) {
|
||||
return AnimatedContainer(
|
||||
curve: Curves.easeInOut,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height: value ? 32 : 16,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.location_on,
|
||||
color: Color.fromARGB(255, 250, 34, 19),
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
Transform(
|
||||
transform: Matrix4.translationValues(0, 21, 0),
|
||||
child: Container(
|
||||
height: 2,
|
||||
width: 12,
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: shadowMenuDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateLocationInfo extends StatefulWidget {
|
||||
const UpdateLocationInfo({super.key});
|
||||
|
||||
@override
|
||||
State<UpdateLocationInfo> createState() => _UpdateLocationInfoState();
|
||||
}
|
||||
|
||||
class _UpdateLocationInfoState extends State<UpdateLocationInfo> {
|
||||
bool showSelectLocationText = false;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
setState(() {
|
||||
showSelectLocationText = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
firstCurve: Curves.easeInOutExpo,
|
||||
secondCurve: Curves.easeInOutExpo,
|
||||
sizeCurve: Curves.easeInOutExpo,
|
||||
crossFadeState: showSelectLocationText
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: Text(
|
||||
S.of(context).selectALocation,
|
||||
style: getEnteTextTheme(context).mini,
|
||||
),
|
||||
secondChild: Text(
|
||||
S.of(context).editsToLocationWillOnlyBeSeenWithinEnte,
|
||||
style: getEnteTextTheme(context).mini,
|
||||
),
|
||||
layoutBuilder: (topChild, topChildKey, bottomChild, bottomChildKey) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
key: bottomChildKey,
|
||||
child: bottomChild,
|
||||
// top: 0,
|
||||
),
|
||||
Positioned(
|
||||
key: topChildKey,
|
||||
child: topChild,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/services/search_service.dart";
|
||||
|
|
|
@ -66,7 +66,7 @@ Future<DateTime?> getCreationTimeFromEXIF(
|
|||
|
||||
Location? locationFromExif(Map<String, IfdTag> exif) {
|
||||
try {
|
||||
return _gpsDataFromExif(exif).toLocationObj();
|
||||
return gpsDataFromExif(exif).toLocationObj();
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to get location from exif", e, s);
|
||||
return null;
|
||||
|
@ -85,7 +85,7 @@ Future<Map<String, IfdTag>> readExifAsync(File file) async {
|
|||
);
|
||||
}
|
||||
|
||||
GPSData _gpsDataFromExif(Map<String, IfdTag> exif) {
|
||||
GPSData gpsDataFromExif(Map<String, IfdTag> exif) {
|
||||
final Map<String, dynamic> exifLocationData = {
|
||||
"lat": null,
|
||||
"long": null,
|
||||
|
|
Loading…
Add table
Reference in a new issue