Merge branch 'main' into horizontal_grid
This commit is contained in:
commit
276b62fae6
74 changed files with 1351 additions and 458 deletions
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 |
1
fastlane/metadata/android/fr/short_description.txt
Normal file
1
fastlane/metadata/android/fr/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
ente est une application de stockage de photos chiffrées de bout en bout
|
1
fastlane/metadata/ios/fr/keywords.txt
Normal file
1
fastlane/metadata/ios/fr/keywords.txt
Normal file
|
@ -0,0 +1 @@
|
|||
photos,photographie,famille,vie privée,cloud,sauvegarde,vidéos,photo,chiffrement,stockage,album,alternative
|
|
@ -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
|
1
fastlane/metadata/playstore/fr/short_description.txt
Normal file
1
fastlane/metadata/playstore/fr/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Stockage de photos chiffrées - sauvegardez, organisez et partagez vos photos et vidéos
|
|
@ -1 +1 @@
|
|||
Archiviazione foto e video crittografata - backup, organizza e condividi album fotografici
|
||||
Archiviazione foto/video criptata - backup, organizza, condividi album
|
|
@ -1 +1 @@
|
|||
Зашифрованное хранилище фото - совершайте резервное копирование и делитесь вашими фото и видео
|
||||
Зашифрованное хранилище фотографий для резервного копирования и обмена
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -12,5 +12,6 @@ enum CollectionMetaEventType {
|
|||
deleted,
|
||||
archived,
|
||||
sortChanged,
|
||||
orderChanged,
|
||||
thumbnailChanged,
|
||||
}
|
||||
|
|
6
lib/generated/intl/messages_de.dart
generated
6
lib/generated/intl/messages_de.dart
generated
|
@ -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"),
|
||||
|
|
21
lib/generated/intl/messages_en.dart
generated
21
lib/generated/intl/messages_en.dart
generated
|
@ -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":
|
||||
|
|
31
lib/generated/intl/messages_it.dart
generated
31
lib/generated/intl/messages_it.dart
generated
|
@ -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"),
|
||||
|
|
92
lib/generated/l10n.dart
generated
92
lib/generated/l10n.dart
generated
|
@ -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> {
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -26,6 +26,7 @@ class File extends EnteFile {
|
|||
int? creationTime;
|
||||
int? modificationTime;
|
||||
int? updationTime;
|
||||
int? addedTime;
|
||||
Location? location;
|
||||
late FileType fileType;
|
||||
int? fileSubType;
|
||||
|
|
|
@ -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],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -95,7 +95,7 @@ class _DeviceFolderVerticalGridViewState
|
|||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisItemCount,
|
||||
crossAxisSpacing: 16.0,
|
||||
childAspectRatio: thumbnailSize / (thumbnailSize + 10),
|
||||
childAspectRatio: thumbnailSize / (thumbnailSize + 22),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -83,6 +83,7 @@ class HomeGalleryWidget extends StatelessWidget {
|
|||
scrollBottomSafeArea: bottomSafeArea + 180,
|
||||
);
|
||||
return Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
gallery,
|
||||
FileSelectionOverlayBar(GalleryType.homepage, selectedFiles)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
228
lib/ui/map/map_pull_up_gallery.dart
Normal file
228
lib/ui/map/map_pull_up_gallery.dart
Normal 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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
94
lib/ui/settings/notification_settings_screen.dart
Normal file
94
lib/ui/settings/notification_settings_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -203,6 +203,7 @@ class _LocationGalleryWidgetState extends State<LocationGalleryWidget> {
|
|||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
Gallery(
|
||||
loadingWidget: Column(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"];
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
24
pubspec.lock
24
pubspec.lock
|
@ -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:
|
||||
|
|
|
@ -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
thirdparty/chewie
vendored
|
@ -1 +0,0 @@
|
|||
Subproject commit f56c85bdcbaa0405f3e62a4408a7a5acbd31deb1
|
1
thirdparty/plugins
vendored
1
thirdparty/plugins
vendored
|
@ -1 +0,0 @@
|
|||
Subproject commit db016cc95c6337766617d644585a835f7693a7df
|
Loading…
Add table
Reference in a new issue