Merge branch 'main' into horizontal_grid

This commit is contained in:
Neeraj Gupta 2023-07-03 10:36:25 +05:30
commit 276b62fae6
74 changed files with 1351 additions and 458 deletions

View file

@ -26,7 +26,7 @@ jobs:
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.7.3'
flutter-version: '3.7.7'
# Fetch sub modules
- run: git submodule update --init --recursive

View file

@ -1,6 +1,6 @@
# CHANGELOG
## Unreleased
## v0.7.71
### Added
* #### Map View ✨
@ -16,10 +16,12 @@
### Improvements
* **Translations**: Add support for German language
* This release contains massive improvements to how smoothly our gallery
scrolls. More improvements are on the way!
## 0.7.62
### Added

View file

@ -111,4 +111,5 @@
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 901 B

After

Width:  |  Height:  |  Size: 886 B

View file

@ -0,0 +1 @@
ente est une application de stockage de photos chiffrées de bout en bout

View file

@ -0,0 +1 @@
photos,photographie,famille,vie privée,cloud,sauvegarde,vidéos,photo,chiffrement,stockage,album,alternative

View file

@ -1 +1 @@
Almacenamiento de fotos encriptado - copias de seguridad, organiza y comprte tus fotos y vídeos
Almacenamiento de fotos encriptadas: copia de seguridad y comparte tus fotos

View file

@ -0,0 +1 @@
Stockage de photos chiffrées - sauvegardez, organisez et partagez vos photos et vidéos

View file

@ -1 +1 @@
Archiviazione foto e video crittografata - backup, organizza e condividi album fotografici
Archiviazione foto/video criptata - backup, organizza, condividi album

View file

@ -1 +1 @@
Зашифрованное хранилище фото - совершайте резервное копирование и делитесь вашими фото и видео
Зашифрованное хранилище фотографий для резервного копирования и обмена

View file

@ -10,6 +10,10 @@ import Flutter
var flutter_native_splash = 1
UIApplication.shared.isStatusBarHidden = false
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

View file

@ -50,6 +50,7 @@ class FilesDB {
static const columnCreationTime = 'creation_time';
static const columnModificationTime = 'modification_time';
static const columnUpdationTime = 'updation_time';
static const columnAddedTime = 'added_time';
static const columnEncryptedKey = 'encrypted_key';
static const columnKeyDecryptionNonce = 'key_decryption_nonce';
static const columnFileDecryptionHeader = 'file_decryption_header';
@ -82,6 +83,7 @@ class FilesDB {
...addFileSizeColumn(),
...updateIndexes(),
...createEntityDataTable(),
...addAddedTime(),
];
final dbConfig = MigrationConfig(
@ -367,6 +369,17 @@ class FilesDB {
];
}
static List<String> addAddedTime() {
return [
'''
ALTER TABLE $filesTable ADD COLUMN $columnAddedTime INTEGER NOT NULL DEFAULT -1;
''',
'''
CREATE INDEX IF NOT EXISTS added_time_index ON $filesTable($columnAddedTime);
'''
];
}
Future<void> clearTable() async {
final db = await instance.database;
await db.delete(filesTable);
@ -627,6 +640,23 @@ class FilesDB {
return files;
}
Future<List<File>> getNewFilesInCollection(
int collectionID,
int addedTime,
) async {
final db = await instance.database;
const String whereClause =
'$columnCollectionID = ? AND $columnAddedTime > ?';
final List<Object> whereArgs = [collectionID, addedTime];
final results = await db.query(
filesTable,
where: whereClause,
whereArgs: whereArgs,
);
final files = convertToFiles(results);
return files;
}
Future<FileLoadResult> getFilesInCollections(
List<int> collectionIDs,
int startTime,
@ -1507,6 +1537,8 @@ class FilesDB {
row[columnCreationTime] = file.creationTime;
row[columnModificationTime] = file.modificationTime;
row[columnUpdationTime] = file.updationTime;
row[columnAddedTime] =
file.addedTime ?? DateTime.now().microsecondsSinceEpoch;
row[columnEncryptedKey] = file.encryptedKey;
row[columnKeyDecryptionNonce] = file.keyDecryptionNonce;
row[columnFileDecryptionHeader] = file.fileDecryptionHeader;
@ -1552,6 +1584,8 @@ class FilesDB {
row[columnCreationTime] = file.creationTime;
row[columnModificationTime] = file.modificationTime;
row[columnUpdationTime] = file.updationTime;
row[columnAddedTime] =
file.addedTime ?? DateTime.now().microsecondsSinceEpoch;
row[columnFileDecryptionHeader] = file.fileDecryptionHeader;
row[columnThumbnailDecryptionHeader] = file.thumbnailDecryptionHeader;
row[columnMetadataDecryptionHeader] = file.metadataDecryptionHeader;
@ -1597,6 +1631,7 @@ class FilesDB {
file.creationTime = row[columnCreationTime];
file.modificationTime = row[columnModificationTime];
file.updationTime = row[columnUpdationTime] ?? -1;
file.addedTime = row[columnAddedTime];
file.encryptedKey = row[columnEncryptedKey];
file.keyDecryptionNonce = row[columnKeyDecryptionNonce];
file.fileDecryptionHeader = row[columnFileDecryptionHeader];

View file

@ -12,5 +12,6 @@ enum CollectionMetaEventType {
deleted,
archived,
sortChanged,
orderChanged,
thumbnailChanged,
}

View file

@ -21,7 +21,7 @@ class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'de';
static String m0(count) =>
"${Intl.plural(count, one: 'Füge ein Element hinzu', other: 'Füge # Elemente hinzu')}";
"${Intl.plural(count, one: 'Element hinzufügen', other: 'Elemente hinzufügen')}";
static String m1(emailOrName) => "Von ${emailOrName} hinzugefügt";
@ -108,7 +108,7 @@ class MessageLookup extends MessageLookupByLibrary {
"${Intl.plural(count, zero: 'keine Erinnerungsstücke', one: '${formattedCount} Erinnerung', other: '${formattedCount} Erinnerungsstücke')}";
static String m30(count) =>
"${Intl.plural(count, one: '# Element', other: '# Elemente')}";
"${Intl.plural(count, one: 'Element verschieben', other: 'Elemente verschieben')}";
static String m31(albumName) => "Erfolgreich zu ${albumName} hinzugefügt";
@ -229,7 +229,7 @@ class MessageLookup extends MessageLookupByLibrary {
"advanced": MessageLookupByLibrary.simpleMessage("Erweitert"),
"advancedSettings": MessageLookupByLibrary.simpleMessage("Erweitert"),
"after1Day": MessageLookupByLibrary.simpleMessage("Nach einem Tag"),
"after1Hour": MessageLookupByLibrary.simpleMessage("Nach 1. Stunde"),
"after1Hour": MessageLookupByLibrary.simpleMessage("Nach 1 Stunde"),
"after1Month": MessageLookupByLibrary.simpleMessage("Nach 1 Monat"),
"after1Week": MessageLookupByLibrary.simpleMessage("Nach 1 Woche"),
"after1Year": MessageLookupByLibrary.simpleMessage("Nach 1 Jahr"),

View file

@ -53,6 +53,9 @@ class MessageLookup extends MessageLookupByLibrary {
static String m10(provider) =>
"Please contact us at support@ente.io to manage your ${provider} subscription.";
static String m63(count) =>
"${Intl.plural(count, one: 'Delete ${count} item', other: 'Delete ${count} items')}";
static String m11(currentlyDeleting, totalCount) =>
"Deleting ${currentlyDeleting} / ${totalCount}";
@ -65,6 +68,9 @@ class MessageLookup extends MessageLookupByLibrary {
static String m14(count, storageSaved) =>
"Your have cleaned up ${Intl.plural(count, one: '${count} duplicate file', other: '${count} duplicate files')}, saving (${storageSaved}!)";
static String m64(count, formattedSize) =>
"${count} files, ${formattedSize} each";
static String m15(newEmail) => "Email changed to ${newEmail}";
static String m16(email) =>
@ -119,6 +125,9 @@ class MessageLookup extends MessageLookupByLibrary {
static String m34(reason) =>
"Unfortunately your payment failed due to ${reason}";
static String m65(endDate) =>
"Free trial valid till ${endDate}.\nYou can choose a paid plan afterwards.";
static String m35(toEmail) => "Please email us at ${toEmail}";
static String m36(toEmail) => "Please send the logs to \n${toEmail}";
@ -484,6 +493,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Delete from device"),
"deleteFromEnte":
MessageLookupByLibrary.simpleMessage("Delete from ente"),
"deleteItemCount": m63,
"deleteLocation":
MessageLookupByLibrary.simpleMessage("Delete location"),
"deletePhotos": MessageLookupByLibrary.simpleMessage("Delete photos"),
@ -541,6 +551,7 @@ class MessageLookup extends MessageLookupByLibrary {
"downloading": MessageLookupByLibrary.simpleMessage("Downloading..."),
"dropSupportEmail": m13,
"duplicateFileCountWithStorageSaved": m14,
"duplicateItemsGroup": m64,
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
"editLocationTagTitle":
MessageLookupByLibrary.simpleMessage("Edit location"),
@ -566,6 +577,8 @@ class MessageLookup extends MessageLookupByLibrary {
"enteCanEncryptAndPreserveFilesOnlyIfYouGrant":
MessageLookupByLibrary.simpleMessage(
"ente can encrypt and preserve files only if you grant access to them"),
"entePhotosPerm": MessageLookupByLibrary.simpleMessage(
"ente <i>needs permission to</i> preserve your photos"),
"enteSubscriptionPitch": MessageLookupByLibrary.simpleMessage(
"ente preserves your memories, so they\'re always available to you, even if you lose your device."),
"enteSubscriptionShareWithFamily": MessageLookupByLibrary.simpleMessage(
@ -830,6 +843,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("No results found"),
"nothingToSeeHere":
MessageLookupByLibrary.simpleMessage("Nothing to see here! 👀"),
"notifications": MessageLookupByLibrary.simpleMessage("Notifications"),
"ok": MessageLookupByLibrary.simpleMessage("Ok"),
"onDevice": MessageLookupByLibrary.simpleMessage("On device"),
"onEnte": MessageLookupByLibrary.simpleMessage(
@ -875,6 +889,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Photos added by you will be removed from the album"),
"pickCenterPoint":
MessageLookupByLibrary.simpleMessage("Pick center point"),
"pinAlbum": MessageLookupByLibrary.simpleMessage("Pin album"),
"playStoreFreeTrialValidTill": m65,
"playstoreSubscription":
MessageLookupByLibrary.simpleMessage("PlayStore subscription"),
"pleaseContactSupportAndWeWillBeHappyToHelp":
@ -1085,6 +1101,10 @@ class MessageLookup extends MessageLookupByLibrary {
"sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage(
"Create shared and collaborative albums with other ente users, including users on free plans."),
"sharedByMe": MessageLookupByLibrary.simpleMessage("Shared by me"),
"sharedPhotoNotifications":
MessageLookupByLibrary.simpleMessage("New shared photos"),
"sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage(
"Receive notifications when someone adds a photo to a shared album that you\'re a part of"),
"sharedWith": m47,
"sharedWithMe": MessageLookupByLibrary.simpleMessage("Shared with me"),
"sharing": MessageLookupByLibrary.simpleMessage("Sharing..."),
@ -1236,6 +1256,7 @@ class MessageLookup extends MessageLookupByLibrary {
"unhidingFilesToAlbum":
MessageLookupByLibrary.simpleMessage("Unhiding files to album"),
"unlock": MessageLookupByLibrary.simpleMessage("Unlock"),
"unpinAlbum": MessageLookupByLibrary.simpleMessage("Unpin album"),
"unselectAll": MessageLookupByLibrary.simpleMessage("Unselect all"),
"update": MessageLookupByLibrary.simpleMessage("Update"),
"updateAvailable":

View file

@ -54,6 +54,9 @@ class MessageLookup extends MessageLookupByLibrary {
static String m10(provider) =>
"Scrivi all\'indirizzo support@ente.io per gestire il tuo abbonamento ${provider}.";
static String m63(count) =>
"${Intl.plural(count, one: 'Elimina ${count} elemento', other: 'Elimina ${count} elementi')}";
static String m11(currentlyDeleting, totalCount) =>
"Eliminazione di ${currentlyDeleting} / ${totalCount}";
@ -66,6 +69,9 @@ class MessageLookup extends MessageLookupByLibrary {
static String m14(count, storageSaved) =>
"Hai ripulito ${Intl.plural(count, one: '${count} doppione', other: '${count} doppioni')}, salvando (${storageSaved}!)";
static String m64(count, formattedSize) =>
"${count} file, ${formattedSize} l\'uno";
static String m15(newEmail) => "Email cambiata in ${newEmail}";
static String m16(email) =>
@ -120,6 +126,9 @@ class MessageLookup extends MessageLookupByLibrary {
static String m34(reason) =>
"Purtroppo il tuo pagamento non è riuscito a causa di ${reason}";
static String m65(endDate) =>
"Prova gratuita valida fino al ${endDate}.\nPuoi scegliere un piano a pagamento in seguito.";
static String m35(toEmail) => "Per favore invia un\'email a ${toEmail}";
static String m36(toEmail) => "Invia i log a \n${toEmail}";
@ -494,6 +503,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Elimina dal dispositivo"),
"deleteFromEnte":
MessageLookupByLibrary.simpleMessage("Elimina da ente"),
"deleteItemCount": m63,
"deleteLocation":
MessageLookupByLibrary.simpleMessage("Elimina posizione"),
"deletePhotos": MessageLookupByLibrary.simpleMessage("Elimina foto"),
@ -553,6 +563,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Scaricamento in corso..."),
"dropSupportEmail": m13,
"duplicateFileCountWithStorageSaved": m14,
"duplicateItemsGroup": m64,
"edit": MessageLookupByLibrary.simpleMessage("Modifica"),
"editLocationTagTitle":
MessageLookupByLibrary.simpleMessage("Modifica luogo"),
@ -566,6 +577,9 @@ class MessageLookup extends MessageLookupByLibrary {
"empty": MessageLookupByLibrary.simpleMessage("Vuoto"),
"emptyTrash":
MessageLookupByLibrary.simpleMessage("Vuoi svuotare il cestino?"),
"enableMaps": MessageLookupByLibrary.simpleMessage("Abilita le Mappe"),
"enableMapsDesc": MessageLookupByLibrary.simpleMessage(
"Questo mostrerà le tue foto su una mappa del mondo.\n\nQuesta mappa è ospitata da Open Street Map e le posizioni esatte delle tue foto non sono mai condivise.\n\nPuoi disabilitare questa funzionalità in qualsiasi momento, dalle Impostazioni."),
"encryptingBackup":
MessageLookupByLibrary.simpleMessage("Crittografando il backup..."),
"encryption": MessageLookupByLibrary.simpleMessage("Crittografia"),
@ -576,6 +590,8 @@ class MessageLookup extends MessageLookupByLibrary {
"enteCanEncryptAndPreserveFilesOnlyIfYouGrant":
MessageLookupByLibrary.simpleMessage(
"ente può criptare e preservare i file solo se concedi l\'accesso alle foto e ai video"),
"entePhotosPerm": MessageLookupByLibrary.simpleMessage(
"ente <i>necessita del permesso per</i> preservare le tue foto"),
"enteSubscriptionPitch": MessageLookupByLibrary.simpleMessage(
"ente conserva i tuoi ricordi, in modo che siano sempre a disposizione, anche se perdi il dispositivo."),
"enteSubscriptionShareWithFamily": MessageLookupByLibrary.simpleMessage(
@ -680,6 +696,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Raggruppa foto nelle vicinanze"),
"hidden": MessageLookupByLibrary.simpleMessage("Nascosti"),
"hide": MessageLookupByLibrary.simpleMessage("Nascondi"),
"hostedAtOsmFrance":
MessageLookupByLibrary.simpleMessage("Ospitato presso OSM France"),
"howItWorks": MessageLookupByLibrary.simpleMessage("Come funziona"),
"howToViewShareeVerificationID": MessageLookupByLibrary.simpleMessage(
"Chiedi di premere a lungo il loro indirizzo email nella schermata delle impostazioni e verificare che gli ID su entrambi i dispositivi corrispondano."),
@ -809,6 +827,8 @@ class MessageLookup extends MessageLookupByLibrary {
"manageParticipants": MessageLookupByLibrary.simpleMessage("Gestisci"),
"manageSubscription":
MessageLookupByLibrary.simpleMessage("Gestisci abbonamento"),
"map": MessageLookupByLibrary.simpleMessage("Mappa"),
"maps": MessageLookupByLibrary.simpleMessage("Mappe"),
"mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"),
"matrix": MessageLookupByLibrary.simpleMessage("Matrix"),
"maxDeviceLimitSpikeHandling": m28,
@ -851,6 +871,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Nessun risultato trovato"),
"nothingToSeeHere":
MessageLookupByLibrary.simpleMessage("Nulla da vedere qui! 👀"),
"notifications": MessageLookupByLibrary.simpleMessage("Notifiche"),
"ok": MessageLookupByLibrary.simpleMessage("Ok"),
"onDevice": MessageLookupByLibrary.simpleMessage("Sul dispositivo"),
"onEnte": MessageLookupByLibrary.simpleMessage(
@ -862,6 +883,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Oops! Qualcosa è andato storto"),
"openTheItem":
MessageLookupByLibrary.simpleMessage("• Apri la foto o il video"),
"openstreetmapContributors": MessageLookupByLibrary.simpleMessage(
"Collaboratori di OpenStreetMap"),
"optionalAsShortAsYouLike": MessageLookupByLibrary.simpleMessage(
"Facoltativo, breve quanto vuoi..."),
"orPickAnExistingOne": MessageLookupByLibrary.simpleMessage(
@ -898,6 +921,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Le foto aggiunte da te verranno rimosse dall\'album"),
"pickCenterPoint": MessageLookupByLibrary.simpleMessage(
"Selezionare il punto centrale"),
"pinAlbum": MessageLookupByLibrary.simpleMessage("Fissa l\'album"),
"playStoreFreeTrialValidTill": m65,
"playstoreSubscription":
MessageLookupByLibrary.simpleMessage("Abbonamento su PlayStore"),
"pleaseContactSupportAndWeWillBeHappyToHelp":
@ -1079,6 +1104,7 @@ class MessageLookup extends MessageLookupByLibrary {
"setAPassword":
MessageLookupByLibrary.simpleMessage("Imposta una password"),
"setAs": MessageLookupByLibrary.simpleMessage("Imposta come"),
"setCover": MessageLookupByLibrary.simpleMessage("Imposta copertina"),
"setLabel": MessageLookupByLibrary.simpleMessage("Imposta"),
"setPasswordTitle":
MessageLookupByLibrary.simpleMessage("Imposta password"),
@ -1107,6 +1133,10 @@ class MessageLookup extends MessageLookupByLibrary {
"sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage(
"Crea album condivisi e collaborativi con altri utenti ente, inclusi utenti su piani gratuiti."),
"sharedByMe": MessageLookupByLibrary.simpleMessage("Condiviso da me"),
"sharedPhotoNotifications":
MessageLookupByLibrary.simpleMessage("Nuove foto condivise"),
"sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage(
"Ricevi notifiche quando qualcuno aggiunge una foto a un album condiviso, di cui fai parte"),
"sharedWith": m47,
"sharedWithMe":
MessageLookupByLibrary.simpleMessage("Condivisi con me"),
@ -1272,6 +1302,7 @@ class MessageLookup extends MessageLookupByLibrary {
"unhidingFilesToAlbum":
MessageLookupByLibrary.simpleMessage("Mostra i file nell\'album"),
"unlock": MessageLookupByLibrary.simpleMessage("Sblocca"),
"unpinAlbum": MessageLookupByLibrary.simpleMessage("Non fissare album"),
"unselectAll":
MessageLookupByLibrary.simpleMessage("Deseleziona tutto"),
"update": MessageLookupByLibrary.simpleMessage("Aggiorna"),

View file

@ -300,6 +300,16 @@ class S {
);
}
/// `ente <i>needs permission to</i> preserve your photos`
String get entePhotosPerm {
return Intl.message(
'ente <i>needs permission to</i> preserve your photos',
name: 'entePhotosPerm',
desc: '',
args: [],
);
}
/// `Ok`
String get ok {
return Intl.message(
@ -2908,6 +2918,28 @@ class S {
);
}
/// `{count, plural, =1 {Delete {count} item} other {Delete {count} items}}`
String deleteItemCount(num count) {
return Intl.plural(
count,
one: 'Delete $count item',
other: 'Delete $count items',
name: 'deleteItemCount',
desc: '',
args: [count],
);
}
/// `{count} files, {formattedSize} each`
String duplicateItemsGroup(int count, String formattedSize) {
return Intl.message(
'$count files, $formattedSize each',
name: 'duplicateItemsGroup',
desc: 'Display the number of duplicate files and their size',
args: [count, formattedSize],
);
}
/// `{count, plural, one{{count} year ago} other{{count} years ago}}`
String yearsAgo(num count) {
return Intl.plural(
@ -3402,6 +3434,36 @@ class S {
);
}
/// `Notifications`
String get notifications {
return Intl.message(
'Notifications',
name: 'notifications',
desc: '',
args: [],
);
}
/// `New shared photos`
String get sharedPhotoNotifications {
return Intl.message(
'New shared photos',
name: 'sharedPhotoNotifications',
desc: '',
args: [],
);
}
/// `Receive notifications when someone adds a photo to a shared album that you're a part of`
String get sharedPhotoNotificationsExplanation {
return Intl.message(
'Receive notifications when someone adds a photo to a shared album that you\'re a part of',
name: 'sharedPhotoNotificationsExplanation',
desc: '',
args: [],
);
}
/// `Advanced`
String get advanced {
return Intl.message(
@ -3812,6 +3874,16 @@ class S {
);
}
/// `Free trial valid till {endDate}.\nYou can choose a paid plan afterwards.`
String playStoreFreeTrialValidTill(Object endDate) {
return Intl.message(
'Free trial valid till $endDate.\nYou can choose a paid plan afterwards.',
name: 'playStoreFreeTrialValidTill',
desc: '',
args: [endDate],
);
}
/// `Your subscription will be cancelled on {endDate}`
String subWillBeCancelledOn(Object endDate) {
return Intl.message(
@ -7342,6 +7414,26 @@ class S {
args: [],
);
}
/// `Unpin album`
String get unpinAlbum {
return Intl.message(
'Unpin album',
name: 'unpinAlbum',
desc: '',
args: [],
);
}
/// `Pin album`
String get pinAlbum {
return Intl.message(
'Pin album',
name: 'pinAlbum',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View file

@ -213,7 +213,7 @@
"@custom": {
"description": "Label for setting custom value for link expiry"
},
"after1Hour": "Nach 1. Stunde",
"after1Hour": "Nach 1 Stunde",
"after1Day": "Nach einem Tag",
"after1Week": "Nach 1 Woche",
"after1Month": "Nach 1 Monat",
@ -708,11 +708,11 @@
"share": "Teilen",
"unhideToAlbum": "Im Album anzeigen",
"restoreToAlbum": "Album wiederherstellen",
"moveItem": "{count, plural, one{# Element} other{# Elemente}}",
"moveItem": "{count, plural, one{Element verschieben} other{Elemente verschieben}}",
"@moveItem": {
"description": "Page title while moving one or more items to an album"
},
"addItem": "{count, plural, one {Füge ein Element hinzu} other {Füge # Elemente hinzu}}",
"addItem": "{count, plural, one {Element hinzufügen} other {Elemente hinzufügen}}",
"@addItem": {
"description": "Page title while adding one or more items to album"
},

View file

@ -24,6 +24,7 @@
"sendEmail": "Send email",
"deleteRequestSLAText": "Your request will be processed within 72 hours.",
"deleteEmailRequest": "Please send an email to <warning>account-deletion@ente.io</warning> from your registered email address.",
"entePhotosPerm": "ente <i>needs permission to</i> preserve your photos",
"ok": "Ok",
"createAccount": "Create account",
"createNewAccount": "Create new account",
@ -417,6 +418,22 @@
"skip": "Skip",
"updatingFolderSelection": "Updating folder selection...",
"itemCount": "{count, plural, one{{count} item} other{{count} items}}",
"deleteItemCount": "{count, plural, =1 {Delete {count} item} other {Delete {count} items}}",
"duplicateItemsGroup": "{count} files, {formattedSize} each",
"@duplicateItemsGroup" : {
"description": "Display the number of duplicate files and their size",
"type": "text",
"placeholders": {
"count": {
"example": "12",
"type": "int"
},
"formattedSize": {
"example": "2.3 MB",
"type": "String"
}
}
},
"yearsAgo": "{count, plural, one{{count} year ago} other{{count} years ago}}",
"backupSettings": "Backup settings",
"backupOverMobileData": "Backup over mobile data",
@ -490,6 +507,9 @@
},
"familyPlans": "Family plans",
"referrals": "Referrals",
"notifications": "Notifications",
"sharedPhotoNotifications": "New shared photos",
"sharedPhotoNotificationsExplanation": "Receive notifications when someone adds a photo to a shared album that you're a part of",
"advanced": "Advanced",
"general": "General",
"security": "Security",
@ -538,6 +558,7 @@
"faqs": "FAQs",
"renewsOn": "Renews on {endDate}",
"freeTrialValidTill": "Free trial valid till {endDate}",
"playStoreFreeTrialValidTill": "Free trial valid till {endDate}.\nYou can choose a paid plan afterwards.",
"subWillBeCancelledOn": "Your subscription will be cancelled on {endDate}",
"subscription": "Subscription",
"paymentDetails": "Payment details",
@ -641,7 +662,6 @@
"description": "Button text for raising a support tickets in case of unhandled errors during backup",
"type": "text"
},
"backupFailed": "Backup failed",
"couldNotBackUpTryLater": "We could not backup your data.\nWe will retry later.",
"enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "ente can encrypt and preserve files only if you grant access to them",
@ -753,7 +773,7 @@
},
"deleteAll": "Delete All",
"renameAlbum": "Rename album",
"setCover" : "Set cover",
"setCover": "Set cover",
"@setCover": {
"description": "Text to set cover photo for an album"
},
@ -955,7 +975,7 @@
"save": "Save",
"centerPoint": "Center point",
"pickCenterPoint": "Pick center point",
"useSelectedPhoto": "Use selected photo",
"useSelectedPhoto": "Use selected photo",
"edit": "Edit",
"deleteLocation": "Delete location",
"rotateLeft": "Rotate left",
@ -982,13 +1002,13 @@
"@storageBreakupYou": {
"description": "Label to indicate how much storage you are using when you are part of a family plan"
},
"storageUsageInfo" : "{usedAmount} {usedStorageUnit} of {totalAmount} {totalStorageUnit} used",
"@storageUsageInfo" :{
"storageUsageInfo": "{usedAmount} {usedStorageUnit} of {totalAmount} {totalStorageUnit} used",
"@storageUsageInfo": {
"description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used"
},
"freeStorageSpace" : "{freeAmount} {storageUnit} free",
"freeStorageSpace": "{freeAmount} {storageUnit} free",
"appVersion": "Version: {versionValue}",
"verifyIDLabel" : "Verify",
"verifyIDLabel": "Verify",
"fileInfoAddDescHint": "Add a description...",
"editLocationTagTitle": "Edit location",
"setLabel": "Set",
@ -1063,6 +1083,7 @@
"selectItemsToAdd": "Select items to add",
"addSelected": "Add selected",
"addFromDevice": "Add from device",
"addPhotos": "Add photos"
"addPhotos": "Add photos",
"unpinAlbum": "Unpin album",
"pinAlbum": "Pin album"
}

View file

@ -24,6 +24,7 @@
"sendEmail": "Invia email",
"deleteRequestSLAText": "La tua richiesta verrà elaborata entro 72 ore.",
"deleteEmailRequest": "Invia un'email a <warning>account-deletion@ente.io</warning> dal tuo indirizzo email registrato.",
"entePhotosPerm": "ente <i>necessita del permesso per</i> preservare le tue foto",
"ok": "Ok",
"createAccount": "Crea account",
"createNewAccount": "Crea un nuovo account",
@ -417,6 +418,22 @@
"skip": "Salta",
"updatingFolderSelection": "Aggiornamento della selezione delle cartelle...",
"itemCount": "{count, plural, one{{count} elemento} other{{count} elementi}}",
"deleteItemCount": "{count, plural, one {}=1 {Elimina {count} elemento} other {Elimina {count} elementi}}",
"duplicateItemsGroup": "{count} file, {formattedSize} l'uno",
"@duplicateItemsGroup": {
"description": "Display the number of duplicate files and their size",
"type": "text",
"placeholders": {
"count": {
"example": "12",
"type": "int"
},
"formattedSize": {
"example": "2.3 MB",
"type": "String"
}
}
},
"yearsAgo": "{count, plural, one{{count} anno fa} other{{count} anni fa}}",
"backupSettings": "Impostazioni backup",
"backupOverMobileData": "Backup su dati mobili",
@ -490,6 +507,9 @@
},
"familyPlans": "Piano famiglia",
"referrals": "Invita un Amico",
"notifications": "Notifiche",
"sharedPhotoNotifications": "Nuove foto condivise",
"sharedPhotoNotificationsExplanation": "Ricevi notifiche quando qualcuno aggiunge una foto a un album condiviso, di cui fai parte",
"advanced": "Avanzate",
"general": "Generali",
"security": "Sicurezza",
@ -538,6 +558,7 @@
"faqs": "FAQ",
"renewsOn": "Si rinnova il {endDate}",
"freeTrialValidTill": "La prova gratuita termina il {endDate}",
"playStoreFreeTrialValidTill": "Prova gratuita valida fino al {endDate}.\nPuoi scegliere un piano a pagamento in seguito.",
"subWillBeCancelledOn": "L'abbonamento verrà cancellato il {endDate}",
"subscription": "Abbonamento",
"paymentDetails": "Dettagli di Pagamento",
@ -752,6 +773,10 @@
},
"deleteAll": "Elimina tutto",
"renameAlbum": "Rinomina album",
"setCover": "Imposta copertina",
"@setCover": {
"description": "Text to set cover photo for an album"
},
"sortAlbumsBy": "Ordina per",
"sortNewestFirst": "Prima le più nuove",
"sortOldestFirst": "Prima le più vecchie",
@ -1044,5 +1069,16 @@
"iOSOkButton": "OK",
"@iOSOkButton": {
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters."
}
},
"openstreetmapContributors": "Collaboratori di OpenStreetMap",
"hostedAtOsmFrance": "Ospitato presso OSM France",
"map": "Mappa",
"@map": {
"description": "Label for the map view"
},
"maps": "Mappe",
"enableMaps": "Abilita le Mappe",
"enableMapsDesc": "Questo mostrerà le tue foto su una mappa del mondo.\n\nQuesta mappa è ospitata da Open Street Map e le posizioni esatte delle tue foto non sono mai condivise.\n\nPuoi disabilitare questa funzionalità in qualsiasi momento, dalle Impostazioni.",
"unpinAlbum": "Non fissare album",
"pinAlbum": "Fissa l'album"
}

View file

@ -12,6 +12,7 @@ extension AppLocalizationsX on BuildContext {
const List<Locale> appSupportedLocales = <Locale>[
Locale('en'),
Locale('es'),
Locale('de'),
Locale('it'),
Locale("nl"),
Locale("zh", "CN"),

View file

@ -29,7 +29,6 @@ import 'package:photos/services/local_file_update_service.dart';
import 'package:photos/services/local_sync_service.dart';
import "package:photos/services/location_service.dart";
import 'package:photos/services/memories_service.dart';
import 'package:photos/services/notification_service.dart';
import "package:photos/services/object_detection/object_detection_service.dart";
import 'package:photos/services/push_service.dart';
import 'package:photos/services/remote_sync_service.dart';
@ -149,6 +148,7 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
final SharedPreferences preferences = await SharedPreferences.getInstance();
await _logFGHeartBeatInfo();
_scheduleHeartBeat(preferences, isBackground);
AppLifecycleService.instance.init(preferences);
if (isBackground) {
AppLifecycleService.instance.onAppInBackground('init via: $via');
} else {
@ -157,7 +157,6 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
// Start workers asynchronously. No need to wait for them to start
Computer.shared().turnOn(workersCount: 4, verbose: kDebugMode);
CryptoUtil.init();
await NotificationService.instance.init();
await NetworkClient.instance.init();
await Configuration.instance.init();
await UserService.instance.init();

View file

@ -13,6 +13,7 @@ class Collection {
final String? keyDecryptionNonce;
@Deprecated("Use collectionName instead")
String? name;
// encryptedName & nameDecryptionNonce will be null for collections
// created before we started encrypting collection name
final String? encryptedName;
@ -28,6 +29,7 @@ class Collection {
// un-encrypted. decryptName will be value either decrypted value for
// encryptedName or name itself.
String? decryptedName;
// decryptedPath will be null for collections now owned by user, deleted
// collections, && collections which don't have a path. The path is used
// to map local on-device album on mobile to remote collection on ente.
@ -98,6 +100,8 @@ class Collection {
// hasSharees returns true if the collection is shared with other ente users
bool get hasSharees => sharees != null && sharees!.isNotEmpty;
bool get isPinned => (magicMetadata.order ?? 0) != 0;
bool isHidden() {
if (isDefaultHidden()) {
return true;

View file

@ -26,6 +26,7 @@ class File extends EnteFile {
int? creationTime;
int? modificationTime;
int? updationTime;
int? addedTime;
Location? location;
late FileType fileType;
int? fileSubType;

View file

@ -11,6 +11,8 @@ const subTypeKey = 'subType';
const muteKey = "mute";
const orderKey = "order";
class CollectionMagicMetadata {
// 0 -> visible
// 1 -> archived
@ -22,13 +24,22 @@ class CollectionMagicMetadata {
// 2 -> Collections created for sharing selected files
int? subType;
CollectionMagicMetadata({required this.visibility, this.subType});
/* order is initially just used for pinned collections.
Later it can be used for custom sort order for if needed.
Higher the value, higher the preference of the collection to show up first.
*/
int? order;
CollectionMagicMetadata({required this.visibility, this.subType, this.order});
Map<String, dynamic> toJson() {
final result = {magicKeyVisibility: visibility};
if (subType != null) {
result[subTypeKey] = subType!;
}
if (order != null) {
result[orderKey] = order!;
}
return result;
}
@ -43,6 +54,7 @@ class CollectionMagicMetadata {
return CollectionMagicMetadata(
visibility: map[magicKeyVisibility] ?? visibleVisibility,
subType: map[subTypeKey],
order: map[orderKey],
);
}
}

View file

@ -71,4 +71,11 @@ class SelectedFiles extends ChangeNotifier {
files.clear();
notifyListeners();
}
/// Retains only the files that are present in the [images] set. Takes the
/// intersection of the two sets.
void filesToRetain(Set<File> images) {
files.retainAll(images);
notifyListeners();
}
}

View file

@ -43,14 +43,11 @@ class UserDetails {
}
int getFreeStorage() {
return max(
isPartOfFamily()
? (familyData!.storage - familyData!.getTotalUsage())
: (subscription.storage - (usage)),
0,
);
return max(getTotalStorage() - getFamilyOrPersonalUsage(), 0);
}
// getTotalStorage will return total storage available including the
// storage bonus
int getTotalStorage() {
return (isPartOfFamily() ? familyData!.storage : subscription.storage) +
storageBonus;

View file

@ -1,18 +1,26 @@
import 'package:logging/logging.dart';
import 'package:media_extension/media_extension_action_types.dart';
import "package:shared_preferences/shared_preferences.dart";
class AppLifecycleService {
static const String keyLastAppOpenTime = "last_app_open_time";
final _logger = Logger("AppLifecycleService");
bool isForeground = false;
MediaExtentionAction mediaExtensionAction =
MediaExtentionAction(action: IntentAction.main);
late SharedPreferences _preferences;
static final AppLifecycleService instance =
AppLifecycleService._privateConstructor();
AppLifecycleService._privateConstructor();
void init(SharedPreferences preferences) {
_preferences = preferences;
}
void setMediaExtensionAction(MediaExtentionAction mediaExtensionAction) {
_logger.info("App invoked via ${mediaExtensionAction.action}");
this.mediaExtensionAction = mediaExtensionAction;
@ -25,6 +33,14 @@ class AppLifecycleService {
void onAppInBackground(String reason) {
_logger.info("App in background $reason");
_preferences.setInt(
keyLastAppOpenTime,
DateTime.now().microsecondsSinceEpoch,
);
isForeground = false;
}
int getLastAppOpenTime() {
return _preferences.getInt(keyLastAppOpenTime) ?? 0;
}
}

View file

@ -1,6 +1,9 @@
import "package:photos/models/file.dart";
import "package:photos/services/filter/filter.dart";
// CollectionsIgnoreFilter will filter out files that are in present in the
// given collections. This is useful for filtering out files that are in archive
// or hidden collections from home page and other places
class CollectionsIgnoreFilter extends Filter {
final Set<int> collectionIDs;
@ -24,7 +27,16 @@ class CollectionsIgnoreFilter extends Filter {
@override
bool filter(File file) {
return file.isUploaded &&
!_ignoredUploadIDs!.contains(file.uploadedFileID!);
if (!file.isUploaded) {
// if file is in one of the ignored collections, filter it out. This check
// avoids showing un-uploaded files that are going to be uploaded to one of
// the ignored collections
if (file.collectionID != null &&
collectionIDs.contains(file.collectionID!)) {
return false;
}
return true;
}
return !_ignoredUploadIDs!.contains(file.uploadedFileID!);
}
}

View file

@ -1,53 +1,123 @@
import 'dart:io';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import "package:photos/services/remote_sync_service.dart";
import "package:shared_preferences/shared_preferences.dart";
class NotificationService {
static final NotificationService instance =
NotificationService._privateConstructor();
static const String keyGrantedNotificationPermission =
"notification_permission_granted";
static const String keyShouldShowNotificationsForSharedPhotos =
"notifications_enabled_shared_photos";
NotificationService._privateConstructor();
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
late SharedPreferences _preferences;
final FlutterLocalNotificationsPlugin _notificationsPlugin =
FlutterLocalNotificationsPlugin();
Future<void> init() async {
if (!Platform.isAndroid) {
return;
}
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('notification_icon');
Future<void> init(
void Function(
NotificationResponse notificationResponse,
)
onNotificationTapped,
) async {
_preferences = await SharedPreferences.getInstance();
const androidSettings = AndroidInitializationSettings('notification_icon');
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: false,
requestSoundPermission: false,
requestBadgePermission: false,
requestCriticalPermission: false,
);
const InitializationSettings initializationSettings =
InitializationSettings(
android: initializationSettingsAndroid,
android: androidSettings,
iOS: iosSettings,
);
await _flutterLocalNotificationsPlugin.initialize(
await _notificationsPlugin.initialize(
initializationSettings,
onSelectNotification: selectNotification,
onDidReceiveNotificationResponse: onNotificationTapped,
);
final launchDetails =
await _notificationsPlugin.getNotificationAppLaunchDetails();
if (launchDetails != null &&
launchDetails.didNotificationLaunchApp &&
launchDetails.notificationResponse != null) {
onNotificationTapped(launchDetails.notificationResponse!);
}
if (!hasGrantedPermissions() &&
RemoteSyncService.instance.isFirstRemoteSyncDone()) {
await requestPermissions();
}
}
Future<void> requestPermissions() async {
bool? result;
if (Platform.isIOS) {
result = await _notificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
sound: true,
alert: true,
);
} else {
result = await _notificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestPermission();
}
if (result != null) {
_preferences.setBool(keyGrantedNotificationPermission, result);
}
}
bool hasGrantedPermissions() {
final result = _preferences.getBool(keyGrantedNotificationPermission);
return result ?? false;
}
bool shouldShowNotificationsForSharedPhotos() {
final result =
_preferences.getBool(keyShouldShowNotificationsForSharedPhotos);
return result ?? true;
}
Future<void> setShouldShowNotificationsForSharedPhotos(bool value) {
return _preferences.setBool(
keyShouldShowNotificationsForSharedPhotos,
value,
);
}
Future selectNotification(String? payload) async {}
Future<void> showNotification(String title, String message) async {
if (!Platform.isAndroid) {
return;
}
const AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails(
'io.ente.photos',
'ente',
Future<void> showNotification(
String title,
String message, {
String channelID = "io.ente.photos",
String channelName = "ente",
String payload = "ente://home",
}) async {
final androidSpecs = AndroidNotificationDetails(
channelID,
channelName,
channelDescription: 'ente alerts',
importance: Importance.max,
priority: Priority.high,
showWhen: false,
);
const NotificationDetails platformChannelSpecifics =
NotificationDetails(android: androidPlatformChannelSpecifics);
await _flutterLocalNotificationsPlugin.show(
0,
final iosSpecs = DarwinNotificationDetails(threadIdentifier: channelID);
final platformChannelSpecs =
NotificationDetails(android: androidSpecs, iOS: iosSpecs);
await _notificationsPlugin.show(
channelName.hashCode,
title,
message,
platformChannelSpecifics,
platformChannelSpecs,
payload: payload,
);
}
}

View file

@ -26,6 +26,7 @@ import 'package:photos/services/collections_service.dart';
import "package:photos/services/feature_flag_service.dart";
import 'package:photos/services/ignored_files_service.dart';
import 'package:photos/services/local_file_update_service.dart';
import "package:photos/services/notification_service.dart";
import 'package:photos/services/sync_service.dart';
import 'package:photos/services/trash_sync_service.dart';
import 'package:photos/utils/diff_fetcher.dart';
@ -170,7 +171,7 @@ class RemoteSyncService {
_logger.info("Pulling remote diff");
final isFirstSync = !_collectionsService.hasSyncedCollections();
if (isFirstSync && !_isExistingSyncSilent) {
Bus.instance.fire(SyncStatusUpdate(SyncStatus.applyingRemoteDiff));
Bus.instance.fire(SyncStatusUpdate(SyncStatus.applyingRemoteDiff));
}
await _collectionsService.sync();
// check and reset user's collection syncTime in past for older clients
@ -183,13 +184,14 @@ class RemoteSyncService {
await _markResetSyncTimeAsDone();
}
await _syncUpdatedCollections();
unawaited(_localFileUpdateService.markUpdatedFilesForReUpload());
}
Future<void> _syncUpdatedCollections() async {
final idsToRemoteUpdationTimeMap =
await _collectionsService.getCollectionIDsToBeSynced();
await _syncUpdatedCollections(idsToRemoteUpdationTimeMap);
unawaited(_localFileUpdateService.markUpdatedFilesForReUpload());
unawaited(_notifyNewFiles(idsToRemoteUpdationTimeMap.keys.toList()));
}
Future<void> _syncUpdatedCollections(final idsToRemoteUpdationTimeMap) async {
for (final cid in idsToRemoteUpdationTimeMap.keys) {
await _syncCollectionDiff(
cid,
@ -621,7 +623,7 @@ class RemoteSyncService {
);
}
/* _storeDiff maps each remoteDiff file to existing
/* _storeDiff maps each remoteFile to existing
entries in files table. When match is found, it compares both file to
perform relevant actions like
[1] Clear local cache when required (Both Shared and Owned files)
@ -637,7 +639,7 @@ class RemoteSyncService {
[Existing]
]
*/
Future _storeDiff(List<File> diff, int collectionID) async {
Future<void> _storeDiff(List<File> diff, int collectionID) async {
int sharedFileNew = 0,
sharedFileUpdated = 0,
localUploadedFromDevice = 0,
@ -648,60 +650,60 @@ class RemoteSyncService {
// this is required when same file is uploaded twice in the same
// collection. Without this check, if both remote files are part of same
// diff response, then we end up inserting one entry instead of two
// as we update the generatedID for remoteDiff to local file's genID
// as we update the generatedID for remoteFile to local file's genID
final Set<int> alreadyClaimedLocalFilesGenID = {};
final List<File> toBeInserted = [];
for (File remoteDiff in diff) {
for (File remoteFile in diff) {
// existingFile will be either set to existing collectionID+localID or
// to the unclaimed aka not already linked to any uploaded file.
File? existingFile;
if (remoteDiff.generatedID != null) {
if (remoteFile.generatedID != null) {
// Case [1] Check and clear local cache when uploadedFile already exist
// Note: Existing file can be null here if it's replaced by the time we
// reach here
existingFile = await _db.getFile(remoteDiff.generatedID!);
existingFile = await _db.getFile(remoteFile.generatedID!);
if (existingFile != null &&
_shouldClearCache(remoteDiff, existingFile)) {
_shouldClearCache(remoteFile, existingFile)) {
needsGalleryReload = true;
await clearCache(remoteDiff);
await clearCache(remoteFile);
}
}
/* If file is not owned by the user, no further processing is required
as Case [2,3,4] are only relevant to files owned by user
*/
if (userID != remoteDiff.ownerID) {
if (userID != remoteFile.ownerID) {
if (existingFile == null) {
sharedFileNew++;
remoteDiff.localID = null;
remoteFile.localID = null;
} else {
sharedFileUpdated++;
// if user has downloaded the file on the device, avoid removing the
// localID reference.
// [Todo-fix: Excluded shared file's localIDs during syncALL]
remoteDiff.localID = existingFile.localID;
remoteFile.localID = existingFile.localID;
}
toBeInserted.add(remoteDiff);
toBeInserted.add(remoteFile);
// end processing for file here, move to next file now
continue;
}
// If remoteDiff is not already synced (i.e. existingFile is null), check
// If remoteFile is not already synced (i.e. existingFile is null), check
// if the remoteFile was uploaded from this device.
// Note: DeviceFolder is ignored for iOS during matching
if (existingFile == null && remoteDiff.localID != null) {
if (existingFile == null && remoteFile.localID != null) {
final localFileEntries = await _db.getUnlinkedLocalMatchesForRemoteFile(
userID,
remoteDiff.localID!,
remoteDiff.fileType,
title: remoteDiff.title ?? '',
deviceFolder: remoteDiff.deviceFolder ?? '',
remoteFile.localID!,
remoteFile.fileType,
title: remoteFile.title ?? '',
deviceFolder: remoteFile.deviceFolder ?? '',
);
if (localFileEntries.isEmpty) {
// set remote file's localID as null because corresponding local file
// does not exist [Case 2, do not retain localID of the remote file]
remoteDiff.localID = null;
remoteFile.localID = null;
} else {
// case 4: Check and schedule the file for update
final int maxModificationTime = localFileEntries
@ -717,11 +719,11 @@ class RemoteSyncService {
for the adjustments or just if the asset has been modified ever.
https://stackoverflow.com/a/50093266/546896
*/
if (maxModificationTime > remoteDiff.modificationTime! &&
if (maxModificationTime > remoteFile.modificationTime! &&
Platform.isAndroid) {
localButUpdatedOnDevice++;
await FileUpdationDB.instance.insertMultiple(
[remoteDiff.localID!],
[remoteFile.localID!],
FileUpdationDB.modificationTimeUpdated,
);
}
@ -738,17 +740,17 @@ class RemoteSyncService {
existingFile = localFileEntries.first;
localUploadedFromDevice++;
alreadyClaimedLocalFilesGenID.add(existingFile.generatedID!);
remoteDiff.generatedID = existingFile.generatedID;
remoteFile.generatedID = existingFile.generatedID;
}
}
}
if (existingFile != null &&
_shouldReloadHomeGallery(remoteDiff, existingFile)) {
_shouldReloadHomeGallery(remoteFile, existingFile)) {
needsGalleryReload = true;
} else {
remoteNewFile++;
}
toBeInserted.add(remoteDiff);
toBeInserted.add(remoteFile);
}
await _db.insertMultiple(toBeInserted);
_logger.info(
@ -850,4 +852,38 @@ class RemoteSyncService {
}
});
}
bool _shouldShowNotification(int collectionID) {
// TODO: Add option to opt out of notifications for a specific collection
// Screen: https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?type=design&node-id=7689-52943&t=IyWOfh0Gsb0p7yVC-4
return NotificationService.instance
.shouldShowNotificationsForSharedPhotos() &&
isFirstRemoteSyncDone() &&
!AppLifecycleService.instance.isForeground;
}
Future<void> _notifyNewFiles(List<int> collectionIDs) async {
final userID = Configuration.instance.getUserID();
final appOpenTime = AppLifecycleService.instance.getLastAppOpenTime();
for (final collectionID in collectionIDs) {
final collection = _collectionsService.getCollectionByID(collectionID);
final files =
await _db.getNewFilesInCollection(collectionID, appOpenTime);
final sharedFileCount =
files.where((file) => file.ownerID != userID).length;
final collectedFileCount = files
.where((file) => file.pubMagicMetadata!.uploaderName != null)
.length;
final totalCount = sharedFileCount + collectedFileCount;
if (totalCount > 0 && _shouldShowNotification(collectionID)) {
NotificationService.instance.showNotification(
collection!.displayName,
totalCount.toString() + " new 📸",
channelID: "collection:" + collectionID.toString(),
channelName: collection.displayName,
payload: "ente://collection/?collectionID=" + collectionID.toString(),
);
}
}
}
}

View file

@ -128,6 +128,13 @@ class UpdateService {
return _packageInfo.packageName.startsWith("io.ente.photos.fdroid");
}
bool isPlayStoreFlavor() {
if (Platform.isIOS) {
return false;
}
return !isIndependentFlavor() && !isFdroidFlavor();
}
// getRateDetails returns details about the place
Tuple2<String, String> getRateDetails() {
if (isFdroidFlavor() || isIndependentFlavor()) {

View file

@ -42,6 +42,7 @@ class _DeleteAccountPageState extends State<DeleteAccountPage> {
Widget build(BuildContext context) {
_defaultSelection = S.of(context).selectReason;
_dropdownValue ??= _defaultSelection;
final double dropDownTextSize = MediaQuery.of(context).size.width - 120;
final colorScheme = getEnteColorScheme(context);
return Scaffold(
@ -90,11 +91,15 @@ class _DeleteAccountPageState extends State<DeleteAccountPage> {
value: value,
enabled: value != _defaultSelection,
alignment: Alignment.centerLeft,
child: Text(
value,
style: value != _defaultSelection
? getEnteTextTheme(context).small
: getEnteTextTheme(context).smallMuted,
child: SizedBox(
width: dropDownTextSize,
child: Text(
value,
style: value != _defaultSelection
? getEnteTextTheme(context).small
: getEnteTextTheme(context).smallMuted,
overflow: TextOverflow.visible,
),
),
);
}).toList(),

View file

@ -253,18 +253,27 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
// action can to be performed
includeCollab: widget.actionType == CollectionActionType.addFiles,
);
collections.removeWhere(
(element) => (element.type == CollectionType.favorites ||
element.type == CollectionType.uncategorized ||
element.isSharedFilesCollection()),
);
collections.sort((first, second) {
return compareAsciiLowerCaseNatural(
first.displayName,
second.displayName,
);
});
return collections;
final List<Collection> pinned = [];
final List<Collection> unpinned = [];
for (final collection in collections) {
if (collection.isSharedFilesCollection() ||
collection.type == CollectionType.favorites ||
collection.type == CollectionType.uncategorized) {
continue;
}
if (collection.isPinned) {
pinned.add(collection);
} else {
unpinned.add(collection);
}
}
return pinned + unpinned;
}
void _removeIncomingCollections(List<Collection> items) {

View file

@ -95,7 +95,7 @@ class _DeviceFolderVerticalGridViewState
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisItemCount,
crossAxisSpacing: 16.0,
childAspectRatio: thumbnailSize / (thumbnailSize + 10),
childAspectRatio: thumbnailSize / (thumbnailSize + 22),
),
),
);

View file

@ -17,6 +17,7 @@ class BottomActionBarWidget extends StatelessWidget {
final VoidCallback? onCancel;
final bool hasSmallerBottomPadding;
final GalleryType type;
final Color? backgroundColor;
BottomActionBarWidget({
required this.expandedMenu,
@ -26,6 +27,7 @@ class BottomActionBarWidget extends StatelessWidget {
this.text,
this.iconButtons,
this.onCancel,
this.backgroundColor,
super.key,
});
@ -42,7 +44,7 @@ class BottomActionBarWidget extends StatelessWidget {
: 0;
return Container(
decoration: BoxDecoration(
color: colorScheme.backgroundElevated,
color: backgroundColor ?? colorScheme.backgroundElevated2,
boxShadow: shadowFloatFaintLight,
),
padding: EdgeInsets.only(

View file

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:photo_manager/photo_manager.dart';
import "package:photos/generated/l10n.dart";
import 'package:photos/services/sync_service.dart';
import "package:styled_text/styled_text.dart";
class GrantPermissionsWidget extends StatelessWidget {
const GrantPermissionsWidget({Key? key}) : super(key: key);
@ -49,24 +50,20 @@ class GrantPermissionsWidget extends StatelessWidget {
const SizedBox(height: 36),
Padding(
padding: const EdgeInsets.fromLTRB(40, 0, 40, 0),
child: RichText(
text: TextSpan(
style: Theme.of(context)
.textTheme
.headlineSmall!
.copyWith(fontWeight: FontWeight.w700),
children: [
const TextSpan(text: 'ente '),
TextSpan(
text: "needs permission to ",
style: Theme.of(context)
.textTheme
.headlineSmall!
.copyWith(fontWeight: FontWeight.w400),
),
const TextSpan(text: 'preserve your photos'),
],
),
child: StyledText(
text: S.of(context).entePhotosPerm,
style: Theme.of(context)
.textTheme
.headlineSmall!
.copyWith(fontWeight: FontWeight.w700),
tags: {
'i': StyledTextTag(
style: Theme.of(context)
.textTheme
.headlineSmall!
.copyWith(fontWeight: FontWeight.w400),
),
},
),
),
],

View file

@ -83,6 +83,7 @@ class HomeGalleryWidget extends StatelessWidget {
scrollBottomSafeArea: bottomSafeArea + 180,
);
return Stack(
alignment: Alignment.bottomCenter,
children: [
gallery,
FileSelectionOverlayBar(GalleryType.homepage, selectedFiles)

View file

@ -293,11 +293,13 @@ class FeatureItemWidget extends StatelessWidget {
Text(
featureTitleFirstLine,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const Padding(padding: EdgeInsets.all(2)),
Text(
featureTitleSecondLine,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const Padding(padding: EdgeInsets.all(12)),
Text(

View file

@ -41,9 +41,9 @@ class _LoadingPhotosWidgetState extends State<LoadingPhotosWidget> {
} else {
routeToPage(
context,
const BackupFolderSelectionPage(
BackupFolderSelectionPage(
isOnboarding: true,
buttonText: "Start backup",
buttonText: S.of(context).startBackup,
),
);
}

View file

@ -62,6 +62,8 @@ class HugeListView<T> extends StatefulWidget {
final bool disableScroll;
final bool isScrollablePositionedList;
const HugeListView({
Key? key,
this.controller,
@ -80,6 +82,7 @@ class HugeListView<T> extends StatefulWidget {
this.isDraggableScrollbarEnabled = true,
this.thumbPadding,
this.disableScroll = false,
this.isScrollablePositionedList = true,
}) : super(key: key);
@override
@ -96,7 +99,9 @@ class HugeListViewState<T> extends State<HugeListView<T>> {
void initState() {
super.initState();
listener.itemPositions.addListener(_sendScroll);
widget.isScrollablePositionedList
? listener.itemPositions.addListener(_sendScroll)
: null;
}
@override
@ -131,52 +136,56 @@ class HugeListViewState<T> extends State<HugeListView<T>> {
return widget.emptyResultBuilder!(context);
}
return LayoutBuilder(
builder: (context, constraints) {
return DraggableScrollbar(
key: scrollKey,
totalCount: widget.totalCount,
initialScrollIndex: widget.startIndex,
onChange: (position) {
final int currentIndex = _currentFirst();
final int floorIndex = (position * widget.totalCount).floor();
final int cielIndex = (position * widget.totalCount).ceil();
int nextIndexToJump;
if (floorIndex != currentIndex && floorIndex > currentIndex) {
nextIndexToJump = floorIndex;
} else if (cielIndex != currentIndex && cielIndex < currentIndex) {
nextIndexToJump = floorIndex;
} else {
return;
}
if (lastIndexJump != nextIndexToJump) {
lastIndexJump = nextIndexToJump;
widget.controller?.jumpTo(index: nextIndexToJump);
}
},
labelTextBuilder: widget.labelTextBuilder,
backgroundColor: widget.thumbBackgroundColor,
drawColor: widget.thumbDrawColor,
heightScrollThumb: widget.thumbHeight,
bottomSafeArea: widget.bottomSafeArea,
currentFirstIndex: _currentFirst(),
isEnabled: widget.isDraggableScrollbarEnabled,
padding: widget.thumbPadding,
child: ScrollablePositionedList.builder(
physics: widget.disableScroll
? const NeverScrollableScrollPhysics()
: null,
itemScrollController: widget.controller,
itemPositionsListener: listener,
return widget.isScrollablePositionedList
? DraggableScrollbar(
key: scrollKey,
totalCount: widget.totalCount,
initialScrollIndex: widget.startIndex,
onChange: (position) {
final int currentIndex = _currentFirst();
final int floorIndex = (position * widget.totalCount).floor();
final int cielIndex = (position * widget.totalCount).ceil();
int nextIndexToJump;
if (floorIndex != currentIndex && floorIndex > currentIndex) {
nextIndexToJump = floorIndex;
} else if (cielIndex != currentIndex &&
cielIndex < currentIndex) {
nextIndexToJump = floorIndex;
} else {
return;
}
if (lastIndexJump != nextIndexToJump) {
lastIndexJump = nextIndexToJump;
widget.controller?.jumpTo(index: nextIndexToJump);
}
},
labelTextBuilder: widget.labelTextBuilder,
backgroundColor: widget.thumbBackgroundColor,
drawColor: widget.thumbDrawColor,
heightScrollThumb: widget.thumbHeight,
bottomSafeArea: widget.bottomSafeArea,
currentFirstIndex: _currentFirst(),
isEnabled: widget.isDraggableScrollbarEnabled,
padding: widget.thumbPadding,
child: ScrollablePositionedList.builder(
physics: widget.disableScroll
? const NeverScrollableScrollPhysics()
: null,
itemScrollController: widget.controller,
itemPositionsListener: listener,
initialScrollIndex: widget.startIndex,
itemCount: max(widget.totalCount, 0),
itemBuilder: (context, index) {
return widget.itemBuilder(context, index);
},
),
)
: ListView.builder(
itemCount: max(widget.totalCount, 0),
itemBuilder: (context, index) {
return widget.itemBuilder(context, index);
},
),
);
},
);
);
}
/// Jump to the [position] in the list. [position] is between 0.0 (first item) and 1.0 (last item), practically currentIndex / totalCount.

View file

@ -1,65 +0,0 @@
import "package:flutter/foundation.dart";
import "package:flutter/material.dart";
import "package:photos/models/file.dart";
import "package:photos/models/file_load_result.dart";
import "package:photos/ui/viewer/file/detail_page.dart";
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
import "package:photos/utils/navigation_util.dart";
class ImageTile extends StatelessWidget {
final File image;
final int index;
final List<File> visibleImages;
const ImageTile({
super.key,
required this.image,
required this.index,
required this.visibleImages,
});
void onTap(BuildContext context, File image, int index) {
if (kDebugMode) {
debugPrint('size of visibleImages: ${visibleImages.length}');
}
final page = DetailPage(
DetailPageConfiguration(
List.unmodifiable(visibleImages),
(
creationStartTime,
creationEndTime, {
limit,
asc,
}) async {
final result = FileLoadResult(visibleImages, false);
return result;
},
index,
'Map',
),
);
routeToPage(
context,
page,
forceCustomPageRoute: true,
);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => onTap(context, image, index),
child: Padding(
padding: const EdgeInsets.fromLTRB(2, 0, 2, 4),
child: SizedBox(
width: 112,
child: ClipRRect(
borderRadius: BorderRadius.circular(2),
child: ThumbnailWidget(image),
),
),
),
);
}
}

View file

@ -0,0 +1,228 @@
import "dart:async";
import "package:defer_pointer/defer_pointer.dart";
import "package:flutter/material.dart";
import "package:logging/logging.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/events/files_updated_event.dart";
import "package:photos/events/local_photos_updated_event.dart";
import "package:photos/models/file.dart";
import "package:photos/models/file_load_result.dart";
import "package:photos/models/gallery_type.dart";
import "package:photos/models/selected_files.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/common/loading_widget.dart";
import "package:photos/ui/viewer/actions/file_selection_overlay_bar.dart";
import "package:photos/ui/viewer/gallery/gallery.dart";
class MapPullUpGallery extends StatefulWidget {
final StreamController<List<File>> visibleImages;
final double bottomUnsafeArea;
final double bottomSheetDraggableAreaHeight;
static const gridCrossAxisSpacing = 4.0;
static const gridMainAxisSpacing = 4.0;
static const gridPadding = 2.0;
static const gridCrossAxisCount = 4;
const MapPullUpGallery(
this.visibleImages,
this.bottomSheetDraggableAreaHeight,
this.bottomUnsafeArea, {
Key? key,
}) : super(key: key);
@override
State<MapPullUpGallery> createState() => _MapPullUpGalleryState();
}
class _MapPullUpGalleryState extends State<MapPullUpGallery> {
final _selectedFiles = SelectedFiles();
@override
Widget build(BuildContext context) {
final Logger logger = Logger("_MapPullUpGalleryState");
final screenHeight = MediaQuery.of(context).size.height;
final unsafeAreaProportion = widget.bottomUnsafeArea / screenHeight;
final double initialChildSize = 0.25 + unsafeAreaProportion;
Widget? cachedScrollableContent;
return DeferredPointerHandler(
child: Stack(
alignment: Alignment.bottomCenter,
clipBehavior: Clip.none,
children: [
DraggableScrollableSheet(
expand: false,
initialChildSize: initialChildSize,
minChildSize: initialChildSize,
maxChildSize: 0.8,
snap: true,
snapSizes: const [0.5],
builder: (context, scrollController) {
//Must use cached widget here to avoid rebuilds when DraggableScrollableSheet
//is snapped to it's initialChildSize
cachedScrollableContent ??=
cacheScrollableContent(scrollController, context, logger);
return cachedScrollableContent!;
},
),
DeferPointer(
child: FileSelectionOverlayBar(
GalleryType.searchResults,
_selectedFiles,
backgroundColor: getEnteColorScheme(context).backgroundElevated2,
),
),
],
),
);
}
Widget cacheScrollableContent(
ScrollController scrollController,
BuildContext context,
logger,
) {
final textTheme = getEnteTextTheme(context);
final colorScheme = getEnteColorScheme(context);
return Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
color: colorScheme.backgroundElevated,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
DraggableHeader(
scrollController: scrollController,
bottomSheetDraggableAreaHeight:
widget.bottomSheetDraggableAreaHeight,
),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeInOutExpo,
switchOutCurve: Curves.easeInOutExpo,
child: StreamBuilder<List<File>>(
stream: widget.visibleImages.stream,
builder: (
BuildContext context,
AsyncSnapshot<List<File>> snapshot,
) {
if (!snapshot.hasData) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.2,
child: const EnteLoadingWidget(),
);
}
final images = snapshot.data!;
logger.info("Visible images: ${images.length}");
//To retain only selected files that are in view (visible)
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_selectedFiles.filesToRetain(images.toSet());
});
if (images.isEmpty) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.2,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"No photos found here",
style: textTheme.large,
),
const SizedBox(height: 4),
Text(
"Zoom out to see photos",
style: textTheme.smallFaint,
)
],
),
),
),
);
}
return AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeInOutExpo,
switchOutCurve: Curves.easeInOutExpo,
child: Gallery(
key: ValueKey(images),
asyncLoader: (
creationStartTime,
creationEndTime, {
limit,
asc,
}) async {
FileLoadResult result;
result = FileLoadResult(images, false);
return result;
},
reloadEvent: Bus.instance.on<LocalPhotosUpdatedEvent>(),
removalEventTypes: const {
EventType.deletedFromRemote,
EventType.deletedFromEverywhere,
},
tagPrefix: "map_gallery",
showSelectAllByDefault: true,
selectedFiles: _selectedFiles,
isScrollablePositionedList: false,
),
);
},
),
),
)
],
),
);
}
}
class DraggableHeader extends StatelessWidget {
const DraggableHeader({
Key? key,
required this.scrollController,
required this.bottomSheetDraggableAreaHeight,
}) : super(key: key);
static const indicatorHeight = 4.0;
final ScrollController scrollController;
final double bottomSheetDraggableAreaHeight;
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
return SingleChildScrollView(
physics: const ClampingScrollPhysics(),
controller: scrollController,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
color: colorScheme.backgroundElevated2,
),
child: Center(
child: Padding(
padding: EdgeInsets.symmetric(
vertical:
bottomSheetDraggableAreaHeight / 2 - indicatorHeight / 2,
),
child: Container(
height: indicatorHeight,
width: 72,
decoration: BoxDecoration(
color: colorScheme.fillBase,
borderRadius: const BorderRadius.all(Radius.circular(2)),
),
),
),
),
),
);
}
}

View file

@ -1,19 +1,18 @@
import "dart:async";
import "dart:isolate";
import "dart:math";
import "package:collection/collection.dart";
import "package:flutter/foundation.dart";
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/models/file.dart";
import "package:photos/models/location/location.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/common/loading_widget.dart";
import "package:photos/ui/map/image_marker.dart";
import 'package:photos/ui/map/image_tile.dart';
import "package:photos/ui/map/map_isolate.dart";
import "package:photos/ui/map/map_pull_up_gallery.dart";
import "package:photos/ui/map/map_view.dart";
import "package:photos/utils/toast_util.dart";
@ -40,14 +39,16 @@ class _MapScreenState extends State<MapScreen> {
StreamController<List<File>>.broadcast();
MapController mapController = MapController();
bool isLoading = true;
double initialZoom = 4.0;
double initialZoom = 4.5;
double maxZoom = 18.0;
double minZoom = 0.0;
double minZoom = 2.8;
int debounceDuration = 500;
LatLng center = LatLng(46.7286, 4.8614);
final Logger _logger = Logger("_MapScreenState");
StreamSubscription? _mapMoveSubscription;
Isolate? isolate;
static const bottomSheetDraggableAreaHeight = 32.0;
List<File>? prevMessage;
@override
void initState() {
@ -72,30 +73,21 @@ class _MapScreenState extends State<MapScreen> {
}
Future<void> processFiles(List<File> files) async {
late double minLat, maxLat, minLon, maxLon;
final List<ImageMarker> tempMarkers = [];
bool hasAnyLocation = false;
File? mostRecentFile;
for (var file in files) {
if (kDebugMode && !file.hasLocation) {
final rand = Random();
file.location = Location(
latitude: 46.7286 + rand.nextDouble() * 0.1,
longitude: 4.8614 + rand.nextDouble() * 0.1,
);
}
if (file.hasLocation && file.location != null) {
if (!hasAnyLocation) {
minLat = file.location!.latitude!;
minLon = file.location!.longitude!;
maxLat = file.location!.latitude!;
maxLon = file.location!.longitude!;
hasAnyLocation = true;
hasAnyLocation = true;
if (mostRecentFile == null) {
mostRecentFile = file;
} else {
minLat = min(minLat, file.location!.latitude!);
minLon = min(minLon, file.location!.longitude!);
maxLat = max(maxLat, file.location!.latitude!);
maxLon = max(maxLon, file.location!.longitude!);
if ((mostRecentFile.creationTime ?? 0) < (file.creationTime ?? 0)) {
mostRecentFile = file;
}
}
tempMarkers.add(
ImageMarker(
latitude: file.location!.latitude!,
@ -108,22 +100,12 @@ class _MapScreenState extends State<MapScreen> {
if (hasAnyLocation) {
center = LatLng(
minLat + (maxLat - minLat) / 2,
minLon + (maxLon - minLon) / 2,
mostRecentFile!.location!.latitude!,
mostRecentFile.location!.longitude!,
);
final latRange = maxLat - minLat;
final lonRange = maxLon - minLon;
final latZoom = log(360.0 / latRange) / log(2);
final lonZoom = log(180.0 / lonRange) / log(2);
initialZoom = min(latZoom, lonZoom);
if (initialZoom <= minZoom) initialZoom = minZoom + 1;
if (initialZoom >= (maxZoom - 1)) initialZoom = maxZoom - 1;
if (kDebugMode) {
debugPrint("Info for map: center $center, initialZoom $initialZoom");
debugPrint("Info for map: minLat $minLat, maxLat $maxLat");
debugPrint("Info for map: minLon $minLon, maxLon $maxLon");
}
} else {
showShortToast(context, "No images with location");
@ -159,7 +141,11 @@ class _MapScreenState extends State<MapScreen> {
_mapMoveSubscription = receivePort.listen((dynamic message) async {
if (message is List<File>) {
visibleImages.sink.add(message);
if (!message.equals(prevMessage ?? [])) {
visibleImages.sink.add(message);
}
prevMessage = message;
} else {
_mapMoveSubscription?.cancel();
isolate?.kill();
@ -188,98 +174,42 @@ class _MapScreenState extends State<MapScreen> {
@override
Widget build(BuildContext context) {
final textTheme = getEnteTextTheme(context);
final colorScheme = getEnteColorScheme(context);
final bottomUnsafeArea = MediaQuery.of(context).padding.bottom;
return Container(
color: colorScheme.backgroundBase,
child: SafeArea(
top: false,
child: Theme(
data: Theme.of(context).copyWith(
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: Colors.transparent,
),
),
child: Scaffold(
body: Stack(
children: [
Column(
children: [
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(6),
bottomRight: Radius.circular(6),
),
child: MapView(
key: ValueKey(
'image-marker-count-${imageMarkers.length}',
),
controller: mapController,
imageMarkers: imageMarkers,
updateVisibleImages: calculateVisibleMarkers,
center: center,
initialZoom: initialZoom,
minZoom: minZoom,
maxZoom: maxZoom,
debounceDuration: debounceDuration,
LayoutBuilder(
builder: (context, constrains) {
return SizedBox(
height: constrains.maxHeight * 0.75 +
bottomSheetDraggableAreaHeight -
bottomUnsafeArea,
child: MapView(
key: ValueKey(
'image-marker-count-${imageMarkers.length}',
),
controller: mapController,
imageMarkers: imageMarkers,
updateVisibleImages: calculateVisibleMarkers,
center: center,
initialZoom: initialZoom,
minZoom: minZoom,
maxZoom: maxZoom,
debounceDuration: debounceDuration,
bottomSheetDraggableAreaHeight:
bottomSheetDraggableAreaHeight,
),
),
const SizedBox(height: 4),
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(2),
topRight: Radius.circular(2),
),
child: SizedBox(
height: 116,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeInOutExpo,
switchOutCurve: Curves.easeInOutExpo,
child: StreamBuilder<List<File>>(
stream: visibleImages.stream,
builder: (
BuildContext context,
AsyncSnapshot<List<File>> snapshot,
) {
if (!snapshot.hasData) {
return const Text("Loading...");
}
final images = snapshot.data!;
_logger.info("Visible images: ${images.length}");
if (images.isEmpty) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"No photos found here",
style: textTheme.large,
),
const SizedBox(height: 4),
Text(
"Zoom out to see photos",
style: textTheme.smallFaint,
)
],
);
}
return ListView.builder(
itemCount: images.length,
scrollDirection: Axis.horizontal,
padding:
const EdgeInsets.symmetric(horizontal: 2),
physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) {
final image = images[index];
return ImageTile(
image: image,
visibleImages: images,
index: index,
);
},
);
},
),
),
),
),
],
);
},
),
isLoading
? EnteLoadingWidget(
@ -289,6 +219,11 @@ class _MapScreenState extends State<MapScreen> {
: const SizedBox.shrink(),
],
),
bottomSheet: MapPullUpGallery(
visibleImages,
bottomSheetDraggableAreaHeight,
bottomUnsafeArea,
),
),
),
);

View file

@ -1,5 +1,3 @@
import "dart:async";
import "package:flutter/material.dart";
import "package:flutter_map/flutter_map.dart";
import "package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart";
@ -10,6 +8,7 @@ import 'package:photos/ui/map/map_gallery_tile.dart';
import 'package:photos/ui/map/map_gallery_tile_badge.dart';
import "package:photos/ui/map/map_marker.dart";
import "package:photos/ui/map/tile/layers.dart";
import "package:photos/utils/debouncer.dart";
class MapView extends StatefulWidget {
final List<ImageMarker> imageMarkers;
@ -20,6 +19,7 @@ class MapView extends StatefulWidget {
final double maxZoom;
final double initialZoom;
final int debounceDuration;
final double bottomSheetDraggableAreaHeight;
const MapView({
Key? key,
@ -31,6 +31,7 @@ class MapView extends StatefulWidget {
required this.maxZoom,
required this.initialZoom,
required this.debounceDuration,
required this.bottomSheetDraggableAreaHeight,
}) : super(key: key);
@override
@ -38,9 +39,9 @@ class MapView extends StatefulWidget {
}
class _MapViewState extends State<MapView> {
Timer? _debounceTimer;
bool _isDebouncing = false;
late List<Marker> _markers;
final _debouncer =
Debouncer(const Duration(milliseconds: 300), executionInterval: 750);
@override
void initState() {
@ -50,23 +51,15 @@ class _MapViewState extends State<MapView> {
@override
void dispose() {
_debounceTimer?.cancel();
_debounceTimer = null;
super.dispose();
}
void onChange(LatLngBounds bounds) {
if (!_isDebouncing) {
_isDebouncing = true;
_debounceTimer?.cancel();
_debounceTimer = Timer(
Duration(milliseconds: widget.debounceDuration),
() {
widget.updateVisibleImages(bounds);
_isDebouncing = false;
},
);
}
_debouncer.run(
() async {
widget.updateVisibleImages(bounds);
},
);
}
@override
@ -81,13 +74,24 @@ class _MapViewState extends State<MapView> {
maxZoom: widget.maxZoom,
enableMultiFingerGestureRace: true,
zoom: widget.initialZoom,
maxBounds: LatLngBounds(
LatLng(-90, -180),
LatLng(90, 180),
),
onPositionChanged: (position, hasGesture) {
if (position.bounds != null) {
onChange(position.bounds!);
}
},
),
nonRotatedChildren: const [OSMFranceTileAttributes()],
nonRotatedChildren: [
Padding(
padding: EdgeInsets.only(
bottom: widget.bottomSheetDraggableAreaHeight,
),
child: const OSMFranceTileAttributes(),
)
],
children: [
const OSMFranceTileLayer(),
MarkerClusterLayerWidget(
@ -97,13 +101,11 @@ class _MapViewState extends State<MapView> {
showPolygon: false,
size: const Size(75, 75),
fitBoundsOptions: const FitBoundsOptions(
padding: EdgeInsets.all(1),
padding: EdgeInsets.all(80),
),
markers: _markers,
onClusterTap: (_) {
if (!_isDebouncing) {
onChange(widget.controller.bounds!);
}
onChange(widget.controller.bounds!);
},
builder: (context, List<Marker> markers) {
final index = int.parse(
@ -143,7 +145,7 @@ class _MapViewState extends State<MapView> {
),
),
Positioned(
bottom: 10,
bottom: widget.bottomSheetDraggableAreaHeight + 10,
right: 10,
child: Column(
children: [

View file

@ -3,6 +3,7 @@ import "dart:async";
import "package:flutter/material.dart";
import "package:flutter_map/plugin_api.dart";
import "package:photos/extensions/list.dart";
import "package:photos/theme/ente_theme.dart";
// Credit: This code is based on the Rich Attribution widget from the flutter_map
class MapAttributionWidget extends StatefulWidget {
@ -123,14 +124,16 @@ class MapAttributionWidgetState extends State<MapAttributionWidget> {
}
WidgetsBinding.instance.addPostFrameCallback(
(_) => WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(
() => persistentAttributionSize =
(persistentAttributionKey.currentContext!.findRenderObject()
as RenderBox)
.size,
),
),
(_) => WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(
() => persistentAttributionSize =
(persistentAttributionKey.currentContext!.findRenderObject()
as RenderBox)
.size,
);
}
}),
);
}
@ -182,6 +185,7 @@ class MapAttributionWidgetState extends State<MapAttributionWidget> {
icon: Icon(
Icons.info_outlined,
size: widget.permanentHeight,
color: getEnteColorScheme(context).backgroundElevated,
),
))(
context,

View file

@ -34,7 +34,7 @@ class OSMFranceTileLayer extends StatelessWidget {
fallbackUrl: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
subdomains: const ['a', 'b', 'c'],
tileProvider: CachedNetworkTileProvider(),
backgroundColor: Colors.transparent,
backgroundColor: const Color.fromARGB(255, 246, 246, 246),
userAgentPackageName: _userAgent,
panBuffer: 1,
);

View file

@ -13,6 +13,7 @@ import 'package:photos/models/billing_plan.dart';
import 'package:photos/models/subscription.dart';
import 'package:photos/models/user_details.dart';
import 'package:photos/services/billing_service.dart';
import "package:photos/services/update_service.dart";
import 'package:photos/services/user_service.dart';
import "package:photos/theme/colors.dart";
import "package:photos/theme/ente_theme.dart";
@ -405,7 +406,7 @@ class _StoreSubscriptionPageState extends State<StoreSubscriptionPage> {
],
),
),
_isFreePlanUser()
_isFreePlanUser() && !UpdateService.instance.isPlayStoreFlavor()
? Text(
S.of(context).twoMonthsFreeOnYearlyPlans,
style: getEnteTextTheme(context).miniMuted,

View file

@ -9,6 +9,7 @@ import 'package:photos/models/billing_plan.dart';
import 'package:photos/models/subscription.dart';
import 'package:photos/models/user_details.dart';
import 'package:photos/services/billing_service.dart';
import "package:photos/services/update_service.dart";
import 'package:photos/services/user_service.dart';
import "package:photos/theme/colors.dart";
import 'package:photos/theme/ente_theme.dart';
@ -541,7 +542,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
],
),
),
_isFreePlanUser()
_isFreePlanUser() && !UpdateService.instance.isPlayStoreFlavor()
? Text(
S.of(context).twoMonthsFreeOnYearlyPlans,
style: getEnteTextTheme(context).miniMuted,

View file

@ -3,6 +3,7 @@ import "package:intl/intl.dart";
import 'package:photos/ente_theme_data.dart';
import "package:photos/generated/l10n.dart";
import 'package:photos/models/subscription.dart';
import "package:photos/services/update_service.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/components/captioned_text_widget.dart";
import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart";
@ -101,7 +102,9 @@ class ValidityWidget extends StatelessWidget {
var message = S.of(context).renewsOn(endDate);
if (currentSubscription!.productID == freeProductID) {
message = S.of(context).freeTrialValidTill(endDate);
message = UpdateService.instance.isPlayStoreFlavor()
? S.of(context).playStoreFreeTrialValidTill(endDate)
: S.of(context).freeTrialValidTill(endDate);
} else if (currentSubscription!.attributes?.isCancelled ?? false) {
message = S.of(context).subWillBeCancelledOn(endDate);
}
@ -110,6 +113,7 @@ class ValidityWidget extends StatelessWidget {
child: Text(
message,
style: Theme.of(context).textTheme.bodySmall,
textAlign: TextAlign.center,
),
);
}

View file

@ -12,6 +12,7 @@ import "package:photos/ui/growth/referral_screen.dart";
import 'package:photos/ui/settings/advanced_settings_screen.dart';
import 'package:photos/ui/settings/common_settings.dart';
import "package:photos/ui/settings/language_picker.dart";
import "package:photos/ui/settings/notification_settings_screen.dart";
import 'package:photos/utils/navigation_util.dart';
class GeneralSectionWidget extends StatelessWidget {
@ -82,6 +83,18 @@ class GeneralSectionWidget extends StatelessWidget {
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).notifications,
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
_onNotificationsTapped(context);
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).advanced,
@ -104,6 +117,13 @@ class GeneralSectionWidget extends StatelessWidget {
BillingService.instance.launchFamilyPortal(context, userDetails);
}
void _onNotificationsTapped(BuildContext context) {
routeToPage(
context,
const NotificationSettingsScreen(),
);
}
void _onAdvancedTapped(BuildContext context) {
routeToPage(
context,

View file

@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import "package:photos/generated/l10n.dart";
import "package:photos/services/notification_service.dart";
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/components/buttons/icon_button_widget.dart';
import 'package:photos/ui/components/captioned_text_widget.dart';
import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
import 'package:photos/ui/components/menu_section_description_widget.dart';
import 'package:photos/ui/components/title_bar_title_widget.dart';
import 'package:photos/ui/components/title_bar_widget.dart';
import 'package:photos/ui/components/toggle_switch_widget.dart';
class NotificationSettingsScreen extends StatelessWidget {
const NotificationSettingsScreen({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
return Scaffold(
body: CustomScrollView(
primary: false,
slivers: <Widget>[
TitleBarWidget(
flexibleSpaceTitle: TitleBarTitleWidget(
title: S.of(context).notifications,
),
actionIcons: [
IconButtonWidget(
icon: Icons.close_outlined,
iconButtonType: IconButtonType.secondary,
onTap: () {
Navigator.pop(context);
Navigator.pop(context);
},
),
],
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Column(
children: [
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).sharedPhotoNotifications,
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: ToggleSwitchWidget(
value: () =>
NotificationService.instance
.hasGrantedPermissions() &&
NotificationService.instance
.shouldShowNotificationsForSharedPhotos(),
onChanged: () async {
await NotificationService.instance
.requestPermissions();
await NotificationService.instance
.setShouldShowNotificationsForSharedPhotos(
!NotificationService.instance
.shouldShowNotificationsForSharedPhotos(),
);
},
),
singleBorderRadius: 8,
alignCaptionedTextToLeft: true,
isGestureDetectorDisabled: true,
),
MenuSectionDescriptionWidget(
content: S
.of(context)
.sharedPhotoNotificationsExplanation,
)
],
)
],
),
),
);
},
childCount: 1,
),
),
],
),
);
}
}

View file

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import "package:flutter_local_notifications/flutter_local_notifications.dart";
import 'package:logging/logging.dart';
import 'package:media_extension/media_extension_action_types.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
@ -13,6 +14,8 @@ import 'package:photos/core/event_bus.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/events/account_configured_event.dart';
import 'package:photos/events/backup_folders_updated_event.dart';
import "package:photos/events/collection_updated_event.dart";
import "package:photos/events/files_updated_event.dart";
import 'package:photos/events/permission_granted_event.dart';
import 'package:photos/events/subscription_purchased_event.dart';
import 'package:photos/events/sync_status_update_event.dart';
@ -20,11 +23,13 @@ import 'package:photos/events/tab_changed_event.dart';
import 'package:photos/events/trigger_logout_event.dart';
import 'package:photos/events/user_logged_out_event.dart';
import "package:photos/generated/l10n.dart";
import "package:photos/models/collection_items.dart";
import 'package:photos/models/selected_files.dart';
import 'package:photos/services/app_lifecycle_service.dart';
import 'package:photos/services/collections_service.dart';
import "package:photos/services/entity_service.dart";
import 'package:photos/services/local_sync_service.dart';
import "package:photos/services/notification_service.dart";
import 'package:photos/services/update_service.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/states/user_details_state.dart';
@ -46,7 +51,9 @@ import 'package:photos/ui/settings/app_update_dialog.dart';
import 'package:photos/ui/settings_page.dart';
import "package:photos/ui/tabs/shared_collections_tab.dart";
import "package:photos/ui/tabs/user_collections_tab.dart";
import "package:photos/ui/viewer/gallery/collection_page.dart";
import 'package:photos/utils/dialog_util.dart';
import "package:photos/utils/navigation_util.dart";
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:uni_links/uni_links.dart';
@ -79,6 +86,7 @@ class _HomeWidgetState extends State<HomeWidget> {
StreamSubscription? _intentDataStreamSubscription;
List<SharedMediaFile>? _sharedFiles;
bool _shouldRenderCreateCollectionSheet = false;
bool _showShowBackupHook = false;
late StreamSubscription<TabChangedEvent> _tabChangedEventSubscription;
late StreamSubscription<SubscriptionPurchasedEvent>
@ -89,6 +97,7 @@ class _HomeWidgetState extends State<HomeWidget> {
late StreamSubscription<SyncStatusUpdate> _firstImportEvent;
late StreamSubscription<BackupFoldersUpdatedEvent> _backupFoldersUpdatedEvent;
late StreamSubscription<AccountConfiguredEvent> _accountConfiguredEvent;
late StreamSubscription<CollectionUpdatedEvent> _collectionUpdatedEvent;
@override
void initState() {
@ -162,6 +171,18 @@ class _HomeWidgetState extends State<HomeWidget> {
setState(() {});
}
});
_collectionUpdatedEvent = Bus.instance.on<CollectionUpdatedEvent>().listen(
(event) async {
// only reset state if backup hook is shown. This is to ensure that
// during first sync, we don't keep showing backup hook if user has
// files
if (mounted &&
_showShowBackupHook &&
event.type == EventType.addedOrUpdated) {
setState(() {});
}
},
);
_initDeepLinks();
UpdateService.instance.shouldUpdate().then((shouldUpdate) {
if (shouldUpdate) {
@ -189,6 +210,8 @@ class _HomeWidgetState extends State<HomeWidget> {
),
);
NotificationService.instance.init(_onDidReceiveNotificationResponse);
super.initState();
}
@ -236,6 +259,7 @@ class _HomeWidgetState extends State<HomeWidget> {
_backupFoldersUpdatedEvent.cancel();
_accountConfiguredEvent.cancel();
_intentDataStreamSubscription?.cancel();
_collectionUpdatedEvent.cancel();
super.dispose();
}
@ -346,7 +370,7 @@ class _HomeWidgetState extends State<HomeWidget> {
});
}
final bool showBackupFolderHook =
_showShowBackupHook =
!Configuration.instance.hasSelectedAnyBackupFolder() &&
!LocalSyncService.instance.hasGrantedLimitedPermissions() &&
CollectionsService.instance.getActiveCollections().isEmpty;
@ -368,7 +392,7 @@ class _HomeWidgetState extends State<HomeWidget> {
openDrawer: Scaffold.of(context).openDrawer,
physics: const BouncingScrollPhysics(),
children: [
showBackupFolderHook
_showShowBackupHook
? const StartBackupHookWidget(headerWidget: _headerWidget)
: HomeGalleryWidget(
header: _headerWidget,
@ -475,4 +499,29 @@ class _HomeWidgetState extends State<HomeWidget> {
// Do not show change dialog again
UpdateService.instance.hideChangeLog().ignore();
}
void _onDidReceiveNotificationResponse(
NotificationResponse notificationResponse,
) async {
final String? payload = notificationResponse.payload;
if (payload != null) {
debugPrint('notification payload: $payload');
final collectionID = Uri.parse(payload).queryParameters["collectionID"];
if (collectionID != null) {
final collection = CollectionsService.instance
.getCollectionByID(int.parse(collectionID))!;
final thumbnail =
await CollectionsService.instance.getCover(collection);
routeToPage(
context,
CollectionPage(
CollectionWithThumbnail(
collection,
thumbnail,
),
),
);
}
}
}
}

View file

@ -9,7 +9,6 @@ import 'package:photos/core/event_bus.dart';
import 'package:photos/events/collection_updated_event.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/events/user_logged_out_event.dart';
import 'package:photos/extensions/list.dart';
import "package:photos/generated/l10n.dart";
import 'package:photos/models/collection.dart';
import 'package:photos/services/collections_service.dart';
@ -90,27 +89,13 @@ class _UserCollectionsTabState extends State<UserCollectionsTab>
Future<List<Collection>> _getCollections() async {
final List<Collection> collections =
CollectionsService.instance.getCollectionsForUI();
// Remove uncategorized collection
collections.removeWhere(
(t) => t.type == CollectionType.uncategorized,
);
final ListMatch<Collection> favMathResult = collections.splitMatch(
(element) => element.type == CollectionType.favorites,
);
// Hide fav collection if it's empty
if (!FavoritesService.instance.hasFavorites()) {
favMathResult.matched.clear();
}
final bool hasFavorites = FavoritesService.instance.hasFavorites();
late Map<int, int> collectionIDToNewestPhotoTime;
if (sortKey == AlbumSortKey.newestPhoto) {
collectionIDToNewestPhotoTime =
await CollectionsService.instance.getCollectionIDToNewestFileTime();
}
favMathResult.unmatched.sort(
collections.sort(
(first, second) {
if (sortKey == AlbumSortKey.albumName) {
return compareAsciiLowerCaseNatural(
@ -127,14 +112,28 @@ class _UserCollectionsTabState extends State<UserCollectionsTab>
}
},
);
// This is a way to identify collection which were automatically created
// during create link flow for selected files
final ListMatch<Collection> potentialSharedLinkCollection =
favMathResult.unmatched.splitMatch(
(e) => (e.isSharedFilesCollection()),
);
final List<Collection> favorites = [];
final List<Collection> pinned = [];
final List<Collection> rest = [];
for (final collection in collections) {
if (collection.type == CollectionType.uncategorized ||
collection.isSharedFilesCollection() ||
collection.isHidden()) {
continue;
}
if (collection.type == CollectionType.favorites) {
// Hide fav collection if it's empty
if (hasFavorites) {
favorites.add(collection);
}
} else if (collection.isPinned) {
pinned.add(collection);
} else {
rest.add(collection);
}
}
return favMathResult.matched + potentialSharedLinkCollection.unmatched;
return favorites + pinned + rest;
}
Widget _getCollectionsGalleryWidget(List<Collection> collections) {

View file

@ -84,11 +84,14 @@ class _PathStorageViewerState extends State<PathStorageViewer> {
subTitleColor: getEnteColorScheme(context).textFaint,
),
trailingWidget: stat != null
? Text(
formatBytes(stat.size),
style: getEnteTextTheme(context)
.small
.copyWith(color: getEnteColorScheme(context).textFaint),
? Padding(
padding: const EdgeInsets.only(left: 12.0),
child: Text(
formatBytes(stat.size),
style: getEnteTextTheme(context)
.small
.copyWith(color: getEnteColorScheme(context).textFaint),
),
)
: SizedBox.fromSize(
size: const Size.square(14),

View file

@ -350,12 +350,7 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
}
Widget _getDeleteButton() {
String text;
if (_selectedFiles.length == 1) {
text = "Delete 1 item";
} else {
text = "Delete " + _selectedFiles.length.toString() + " items";
}
final String text = S.of(context).deleteItemCount(_selectedFiles.length);
int size = 0;
for (final file in _selectedFiles) {
size += _fileSizeMap[file.uploadedFileID]!;
@ -416,10 +411,10 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
Padding(
padding: const EdgeInsets.fromLTRB(2, 4, 4, 12),
child: Text(
duplicates.files.length.toString() +
" files, " +
formatBytes(duplicates.size) +
" each",
S.of(context).duplicateItemsGroup(
duplicates.files.length,
formatBytes(duplicates.size),
),
style: Theme.of(context).textTheme.titleSmall,
),
),

View file

@ -19,12 +19,14 @@ class FileSelectionOverlayBar extends StatefulWidget {
final SelectedFiles selectedFiles;
final Collection? collection;
final DeviceCollection? deviceCollection;
final Color? backgroundColor;
const FileSelectionOverlayBar(
this.galleryType,
this.selectedFiles, {
this.collection,
this.deviceCollection,
this.backgroundColor,
Key? key,
}) : super(key: key);
@ -35,14 +37,14 @@ class FileSelectionOverlayBar extends StatefulWidget {
class _FileSelectionOverlayBarState extends State<FileSelectionOverlayBar> {
final GlobalKey shareButtonKey = GlobalKey();
final ValueNotifier<double> _bottomPosition = ValueNotifier(-150.0);
final ValueNotifier<bool> _hasSelectedFilesNotifier = ValueNotifier(false);
late bool showDeleteOption;
@override
void initState() {
super.initState();
showDeleteOption = widget.galleryType.showDeleteIconOption();
widget.selectedFiles.addListener(_selectedFilesListener);
super.initState();
}
@override
@ -125,15 +127,17 @@ class _FileSelectionOverlayBarState extends State<FileSelectionOverlayBar> {
),
);
return ValueListenableBuilder(
valueListenable: _bottomPosition,
valueListenable: _hasSelectedFilesNotifier,
builder: (context, value, child) {
return AnimatedPositioned(
curve: Curves.easeInOutExpo,
bottom: _bottomPosition.value,
right: 0,
left: 0,
return AnimatedCrossFade(
firstCurve: Curves.easeInOutExpo,
secondCurve: Curves.easeInOutExpo,
sizeCurve: Curves.easeInOutExpo,
crossFadeState: _hasSelectedFilesNotifier.value
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(milliseconds: 400),
child: BottomActionBarWidget(
firstChild: BottomActionBarWidget(
selectedFiles: widget.selectedFiles,
hasSmallerBottomPadding: true,
type: widget.galleryType,
@ -151,7 +155,9 @@ class _FileSelectionOverlayBarState extends State<FileSelectionOverlayBar> {
}
},
iconButtons: iconsButton,
backgroundColor: widget.backgroundColor,
),
secondChild: const SizedBox(width: double.infinity),
);
},
);
@ -167,8 +173,6 @@ class _FileSelectionOverlayBarState extends State<FileSelectionOverlayBar> {
}
_selectedFilesListener() {
widget.selectedFiles.files.isNotEmpty
? _bottomPosition.value = 0.0
: _bottomPosition.value = -150.0;
_hasSelectedFilesNotifier.value = widget.selectedFiles.files.isNotEmpty;
}
}

View file

@ -1,3 +1,5 @@
import 'dart:math' as math;
import "package:flutter/cupertino.dart";
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
import "package:photos/generated/l10n.dart";
@ -80,6 +82,19 @@ class ArchiveOverlayIcon extends StatelessWidget {
}
}
class PinOverlayIcon extends StatelessWidget {
const PinOverlayIcon({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const _BottomRightOverlayIcon(
CupertinoIcons.pin,
color: fixedStrokeMutedWhite,
rotationAngle: 45 * math.pi / 180,
);
}
}
class LivePhotoOverlayIcon extends StatelessWidget {
const LivePhotoOverlayIcon({Key? key}) : super(key: key);
@ -243,9 +258,13 @@ class _BottomRightOverlayIcon extends StatelessWidget {
/// smaller thumbnails).
final double baseSize;
// Overridable rotation angle. Default is null, which means no rotation.
final double? rotationAngle;
const _BottomRightOverlayIcon(
this.icon, {
Key? key,
this.rotationAngle,
this.baseSize = 24,
this.color = Colors.white, // fixed
}) : super(key: key);
@ -284,11 +303,20 @@ class _BottomRightOverlayIcon extends StatelessWidget {
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.only(bottom: inset, right: inset),
child: Icon(
icon,
size: size,
color: color,
),
child: rotationAngle == null
? Icon(
icon,
size: size,
color: color,
)
: Transform.rotate(
angle: rotationAngle!, // rotate by 45 degrees
child: Icon(
icon,
size: size,
color: color,
),
),
),
),
);

View file

@ -24,6 +24,7 @@ class ThumbnailWidget extends StatefulWidget {
final BoxFit fit;
final bool shouldShowSyncStatus;
final bool shouldShowArchiveStatus;
final bool shouldShowPinIcon;
final bool showFavForAlbumOnly;
final bool shouldShowLivePhotoOverlay;
final Duration? diskLoadDeferDuration;
@ -38,6 +39,7 @@ class ThumbnailWidget extends StatefulWidget {
this.shouldShowSyncStatus = true,
this.shouldShowLivePhotoOverlay = false,
this.shouldShowArchiveStatus = false,
this.shouldShowPinIcon = false,
this.showFavForAlbumOnly = false,
this.shouldShowOwnerAvatar = false,
this.diskLoadDeferDuration,
@ -182,6 +184,9 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
if (widget.shouldShowArchiveStatus) {
viewChildren.add(const ArchiveOverlayIcon());
}
if (widget.shouldShowPinIcon) {
viewChildren.add(const PinOverlayIcon());
}
return Stack(
fit: StackFit.expand,

View file

@ -12,6 +12,7 @@ import 'package:photos/models/file.dart';
import 'package:photos/services/files_service.dart';
import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
import 'package:photos/ui/viewer/file/video_controls.dart';
import "package:photos/utils/dialog_util.dart";
import 'package:photos/utils/file_util.dart';
import 'package:photos/utils/toast_util.dart';
import 'package:video_player/video_player.dart';
@ -135,12 +136,29 @@ class _VideoWidgetState extends State<VideoWidget> {
} else {
videoPlayerController = VideoPlayerController.file(file!);
}
return _videoPlayerController = videoPlayerController
debugPrint("videoPlayerController: $videoPlayerController");
_videoPlayerController = videoPlayerController
..initialize().whenComplete(() {
if (mounted) {
setState(() {});
}
});
}).onError(
(error, stackTrace) {
if (mounted) {
if (error is Exception) {
showErrorDialogForException(
context: context,
exception: error,
message: "Failed to play video\n ${error.toString()}",
);
} else {
showToast(context, "Failed to play video");
}
}
},
);
return videoPlayerController;
}
@override

View file

@ -1,5 +1,6 @@
import "dart:math";
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import "package:photos/theme/ente_theme.dart";
@ -19,7 +20,11 @@ class PlaceHolderGridViewWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final Color faintColor = getEnteColorScheme(context).fillFaint;
final int limitCount = min(count, columns * 5);
int limitCount = count;
if (kDebugMode) {
limitCount = min(count, columns * 5);
}
final key = '$limitCount:$columns';
if (!_gridViewCache.containsKey(key)) {
_gridViewCache[key] = GridView.builder(

View file

@ -40,6 +40,7 @@ class MultipleGroupsGalleryView extends StatelessWidget {
final String logTag;
final Logger logger;
final bool showSelectAllByDefault;
final bool isScrollablePositionedList;
const MultipleGroupsGalleryView({
required this.hugeListViewKey,
@ -60,6 +61,7 @@ class MultipleGroupsGalleryView extends StatelessWidget {
required this.logTag,
required this.logger,
required this.showSelectAllByDefault,
required this.isScrollablePositionedList,
super.key,
});
@ -72,6 +74,7 @@ class MultipleGroupsGalleryView extends StatelessWidget {
totalCount: groupedFiles.length,
isDraggableScrollbarEnabled: groupedFiles.length > 10,
disableScroll: disableScroll,
isScrollablePositionedList: isScrollablePositionedList,
waitBuilder: (_) {
return const EnteLoadingWidget();
},

View file

@ -49,6 +49,7 @@ class Gallery extends StatefulWidget {
// will select if even when no other item is selected.
final bool inSelectionMode;
final bool showSelectAllByDefault;
final bool isScrollablePositionedList;
// add a Function variable to get sort value in bool
final SortAscFn? sortAsyncFn;
@ -73,6 +74,7 @@ class Gallery extends StatefulWidget {
this.inSelectionMode = false,
this.sortAsyncFn,
this.showSelectAllByDefault = true,
this.isScrollablePositionedList = true,
Key? key,
}) : super(key: key);
@ -247,6 +249,7 @@ class _GalleryState extends State<Gallery> {
footer: widget.footer,
selectedFiles: widget.selectedFiles,
showSelectAllByDefault: widget.showSelectAllByDefault,
isScrollablePositionedList: widget.isScrollablePositionedList,
),
);
}

View file

@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:io';
import 'dart:math' as math;
import "package:flutter/cupertino.dart";
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
@ -66,6 +68,7 @@ enum AlbumPopupAction {
freeUpSpace,
setCover,
addPhotos,
pinAlbum,
}
class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
@ -368,6 +371,31 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
),
),
);
items.add(
PopupMenuItem(
value: AlbumPopupAction.pinAlbum,
child: Row(
children: [
widget.collection!.isPinned
? const Icon(CupertinoIcons.pin_slash)
: Transform.rotate(
angle: 45 * math.pi / 180, // rotate by 45 degrees
child: const Icon(CupertinoIcons.pin),
),
const Padding(
padding: EdgeInsets.all(8),
),
Text(
widget.collection!.isPinned
? S.of(context).unpinAlbum
: S.of(context).pinAlbum,
),
],
),
),
);
items.add(
PopupMenuItem(
value: AlbumPopupAction.ownedArchive,
@ -467,6 +495,13 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
onSelected: (AlbumPopupAction value) async {
if (value == AlbumPopupAction.rename) {
await _renameAlbum(context);
} else if (value == AlbumPopupAction.pinAlbum) {
await updateOrder(
context,
widget.collection!,
widget.collection!.isPinned ? 0 : 1,
);
if (mounted) setState(() {});
} else if (value == AlbumPopupAction.ownedArchive) {
await changeCollectionVisibility(
context,

View file

@ -203,6 +203,7 @@ class _LocationGalleryWidgetState extends State<LocationGalleryWidget> {
builder: (context, snapshot) {
if (snapshot.hasData) {
return Stack(
alignment: Alignment.bottomCenter,
children: [
Gallery(
loadingWidget: Column(

View file

@ -1,15 +1,26 @@
import 'dart:async';
import 'package:flutter/material.dart';
import "package:photos/models/typedefs.dart";
class Debouncer {
final Duration _duration;
final ValueNotifier<bool> _debounceActiveNotifier = ValueNotifier(false);
/// If executionInterval is not null, then the debouncer will execute the
/// current callback it has in run() method repeatedly in the given interval.
final int? executionInterval;
Timer? _debounceTimer;
Debouncer(this._duration);
Debouncer(this._duration, {this.executionInterval});
final Stopwatch _stopwatch = Stopwatch();
void run(FutureVoidCallback fn) {
if (executionInterval != null) {
runCallbackIfIntervalTimeElapses(fn);
}
void run(Future<void> Function() fn) {
if (isActive()) {
_debounceTimer!.cancel();
}
@ -26,6 +37,14 @@ class Debouncer {
}
}
runCallbackIfIntervalTimeElapses(FutureVoidCallback fn) {
_stopwatch.isRunning ? null : _stopwatch.start();
if (_stopwatch.elapsedMilliseconds > executionInterval!) {
_stopwatch.reset();
fn();
}
}
bool isActive() => _debounceTimer != null && _debounceTimer!.isActive;
ValueNotifier<bool> get debounceActiveNotifier {

View file

@ -46,8 +46,10 @@ Future<ButtonResult?> showErrorDialogForException({
required Exception exception,
bool isDismissible = true,
String apiErrorPrefix = "It looks like something went wrong.",
String? message,
}) async {
String errorMessage = S.of(context).tempErrorContactSupportIfPersists;
String errorMessage =
message ?? S.of(context).tempErrorContactSupportIfPersists;
if (exception is DioError &&
exception.response != null &&
exception.response!.data["code"] != null) {

View file

@ -53,6 +53,7 @@ class DiffFetcher {
.getUploadedFile(file.uploadedFileID!, file.collectionID!);
if (existingFile != null) {
file.generatedID = existingFile.generatedID;
file.addedTime = existingFile.addedTime;
}
}
file.ownerID = item["ownerID"];

View file

@ -25,10 +25,12 @@ import 'package:photos/models/file.dart';
import 'package:photos/models/file_type.dart';
import "package:photos/models/metadata/file_magic.dart";
import 'package:photos/models/upload_url.dart';
import "package:photos/models/user_details.dart";
import 'package:photos/services/collections_service.dart';
import "package:photos/services/file_magic_service.dart";
import 'package:photos/services/local_sync_service.dart';
import 'package:photos/services/sync_service.dart';
import "package:photos/services/user_service.dart";
import 'package:photos/utils/crypto_util.dart';
import 'package:photos/utils/file_download_util.dart';
import 'package:photos/utils/file_uploader_util.dart';
@ -42,6 +44,7 @@ class FileUploader {
static const kMaximumUploadAttempts = 4;
static const kBlockedUploadsPollFrequency = Duration(seconds: 2);
static const kFileUploadTimeout = Duration(minutes: 50);
static const k20MBStorageBuffer = 20 * 1024 * 1024;
final _logger = Logger("FileUploader");
final _dio = NetworkClient.instance.getDio();
@ -396,6 +399,7 @@ class FileUploader {
if (io.File(encryptedFilePath).existsSync()) {
await io.File(encryptedFilePath).delete();
}
await _checkIfWithinStorageLimit(mediaUploadData!.sourceFile!);
final encryptedFile = io.File(encryptedFilePath);
final EncryptionResult fileAttributes = await CryptoUtil.encryptFile(
mediaUploadData!.sourceFile!.path,
@ -703,6 +707,39 @@ class FileUploader {
await _uploadLocks.releaseLock(lockKey, _processType.toString());
}
/*
_checkIfWithinStorageLimit verifies if the file size for encryption and upload
is within the storage limit. It throws StorageLimitExceededError if the limit
is exceeded. This check is best effort and may not be completely accurate
due to UserDetail cache. It prevents infinite loops when clients attempt to
upload files that exceed the server's storage limit + buffer.
Note: Local storageBuffer is 20MB, server storageBuffer is 50MB, and an
additional 30MB is reserved for thumbnails and encryption overhead.
*/
Future<void> _checkIfWithinStorageLimit(io.File fileToBeUploaded) async {
try {
final UserDetails? userDetails =
UserService.instance.getCachedUserDetails();
if (userDetails == null) {
return;
}
// add k20MBStorageBuffer to the free storage
final num freeStorage = userDetails.getFreeStorage() + k20MBStorageBuffer;
final num fileSize = await fileToBeUploaded.length();
if (fileSize > freeStorage) {
_logger.warning('Storage limit exceeded fileSize $fileSize and '
'freeStorage $freeStorage');
throw StorageLimitExceededError();
}
} catch (e) {
if (e is StorageLimitExceededError) {
rethrow;
} else {
_logger.severe('Error checking storage limit', e);
}
}
}
Future _onInvalidFileError(File file, InvalidFileError e) async {
final String ext = file.title == null ? "no title" : extension(file.title!);
_logger.severe(

View file

@ -9,6 +9,7 @@ import 'package:photos/events/force_reload_home_gallery_event.dart';
import "package:photos/generated/l10n.dart";
import 'package:photos/models/collection.dart';
import 'package:photos/models/file.dart';
import "package:photos/models/metadata/collection_magic.dart";
import "package:photos/models/metadata/common_keys.dart";
import "package:photos/models/metadata/file_magic.dart";
import 'package:photos/services/collections_service.dart';
@ -105,6 +106,26 @@ Future<void> changeSortOrder(
}
}
Future<void> updateOrder(
BuildContext context,
Collection collection,
int order,
) async {
try {
final Map<String, dynamic> update = {
orderKey: order,
};
await CollectionsService.instance.updateMagicMetadata(collection, update);
Bus.instance.fire(
CollectionMetaEvent(collection.id, CollectionMetaEventType.orderChanged),
);
} catch (e, s) {
_logger.severe("failed to update order", e, s);
showShortToast(context, S.of(context).somethingWentWrong);
rethrow;
}
}
Future<void> changeCoverPhoto(
BuildContext context,
Collection collection,

View file

@ -379,6 +379,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.4"
defer_pointer:
dependency: "direct main"
description:
name: defer_pointer
sha256: d69e6f8c1d0f052d2616cc1db3782e0ea73f42e4c6f6122fd1a548dfe79faf02
url: "https://pub.dev"
source: hosted
version: "0.0.2"
device_info:
dependency: "direct main"
description:
@ -713,26 +721,26 @@ packages:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "57d0012730780fe137260dd180e072c18a73fbeeb924cdc029c18aaa0f338d64"
sha256: f222919a34545931e47b06000836b5101baeffb0e6eb5a4691d2d42851740dd9
url: "https://pub.dev"
source: hosted
version: "9.9.1"
version: "12.0.4"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: b472bfc173791b59ede323661eae20f7fff0b6908fea33dd720a6ef5d576bae8
sha256: "6af440e3962eeab8459602c309d7d4ab9e62f05d5cfe58195a28f846a0b5d523"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "1.0.0"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "21bceee103a66a53b30ea9daf677f990e5b9e89b62f222e60dd241cd08d63d3a"
sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
version: "6.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
@ -2026,10 +2034,10 @@ packages:
dependency: transitive
description:
name: timezone
sha256: "57b35f6e8ef731f18529695bffc62f92c6189fac2e52c12d478dec1931afb66e"
sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0"
url: "https://pub.dev"
source: hosted
version: "0.8.0"
version: "0.9.2"
timing:
dependency: transitive
description:

View file

@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.7.69+469
version: 0.7.75+476
environment:
sdk: '>=2.17.0 <3.0.0'
@ -36,6 +36,7 @@ dependencies:
connectivity_plus: ^3.0.3
crypto: ^3.0.2
cupertino_icons: ^1.0.0
defer_pointer: ^0.0.2
device_info: ^2.0.2
dio: ^4.0.6
dots_indicator: ^2.0.0
@ -62,7 +63,7 @@ dependencies:
flutter_image_compress: ^1.1.0
flutter_inappwebview: ^5.5.0+2
flutter_launcher_icons: ^0.9.3
flutter_local_notifications: ^9.5.3+1
flutter_local_notifications: ^12.0.4
flutter_localizations:
sdk: flutter
flutter_map: ^4.0.0

1
thirdparty/chewie vendored

@ -1 +0,0 @@
Subproject commit f56c85bdcbaa0405f3e62a4408a7a5acbd31deb1

1
thirdparty/plugins vendored

@ -1 +0,0 @@
Subproject commit db016cc95c6337766617d644585a835f7693a7df