Kaynağa Gözat

Merge branch 'main' into onnx

vishnukvmd 1 yıl önce
ebeveyn
işleme
0408275ed0
74 değiştirilmiş dosya ile 954 ekleme ve 307 silme
  1. 1 1
      analysis_options.yaml
  2. 36 0
      fastlane/metadata/android/de/full_description.txt
  3. 1 0
      fastlane/metadata/android/pl/short_description.txt
  4. 1 0
      fastlane/metadata/android/pl/title.txt
  5. 33 0
      fastlane/metadata/ios/pl/description.txt
  6. 1 0
      fastlane/metadata/ios/pl/keywords.txt
  7. 1 0
      fastlane/metadata/ios/pl/name.txt
  8. 1 0
      fastlane/metadata/ios/pl/subtitle.txt
  9. 1 0
      fastlane/metadata/playstore/pl/short_description.txt
  10. 1 0
      fastlane/metadata/playstore/pl/title.txt
  11. 1 1
      lib/core/configuration.dart
  12. 3 3
      lib/core/error-reporting/super_logging.dart
  13. 1 0
      lib/core/errors.dart
  14. 13 0
      lib/db/file_updation_db.dart
  15. 18 0
      lib/db/files_db.dart
  16. 1 1
      lib/generated/l10n.dart
  17. 60 8
      lib/l10n/intl_de.arb
  18. 2 2
      lib/l10n/intl_en.arb
  19. 2 2
      lib/l10n/intl_nl.arb
  20. 2 2
      lib/l10n/intl_zh.arb
  21. 4 4
      lib/main.dart
  22. 3 3
      lib/models/search/search_types.dart
  23. 2 2
      lib/services/billing_service.dart
  24. 1 1
      lib/services/entity_service.dart
  25. 9 5
      lib/services/files_service.dart
  26. 1 1
      lib/services/hidden_service.dart
  27. 9 5
      lib/services/local_authentication_service.dart
  28. 205 5
      lib/services/local_file_update_service.dart
  29. 35 3
      lib/services/remote_sync_service.dart
  30. 9 8
      lib/services/user_service.dart
  31. 1 1
      lib/ui/account/delete_account_page.dart
  32. 3 3
      lib/ui/account/password_entry_page.dart
  33. 1 1
      lib/ui/account/verify_recovery_page.dart
  34. 5 2
      lib/ui/actions/collection/collection_file_actions.dart
  35. 56 11
      lib/ui/actions/collection/collection_sharing_actions.dart
  36. 4 1
      lib/ui/actions/file/file_actions.dart
  37. 6 6
      lib/ui/collections/album/vertical_list.dart
  38. 1 1
      lib/ui/collections/new_album_icon.dart
  39. 1 2
      lib/ui/components/bottom_action_bar/bottom_action_bar_widget.dart
  40. 1 1
      lib/ui/components/buttons/button_widget.dart
  41. 6 1
      lib/ui/home/status_bar_widget.dart
  42. 1 1
      lib/ui/payment/payment_web_page.dart
  43. 1 1
      lib/ui/payment/store_subscription_page.dart
  44. 21 17
      lib/ui/payment/stripe_subscription_page.dart
  45. 1 1
      lib/ui/payment/view_add_on_widget.dart
  46. 6 2
      lib/ui/settings/advanced_settings_screen.dart
  47. 1 1
      lib/ui/settings/backup/backup_folder_selection_page.dart
  48. 4 4
      lib/ui/settings/backup/backup_section_widget.dart
  49. 3 3
      lib/ui/settings/debug_section_widget.dart
  50. 1 1
      lib/ui/settings/general_section_widget.dart
  51. 1 1
      lib/ui/settings/security_section_widget.dart
  52. 4 1
      lib/ui/sharing/manage_album_participant.dart
  53. 1 1
      lib/ui/sharing/pickers/device_limit_picker_page.dart
  54. 1 1
      lib/ui/sharing/pickers/link_expiry_picker_page.dart
  55. 62 47
      lib/ui/viewer/actions/file_selection_actions_widget.dart
  56. 6 6
      lib/ui/viewer/file/file_app_bar.dart
  57. 5 1
      lib/ui/viewer/file/video_widget_new.dart
  58. 17 17
      lib/ui/viewer/file/zoomable_image.dart
  59. 4 0
      lib/ui/viewer/file/zoomable_live_image_new.dart
  60. 7 3
      lib/ui/viewer/file_details/favorite_widget.dart
  61. 1 1
      lib/ui/viewer/gallery/empty_album_state.dart
  62. 2 0
      lib/ui/viewer/gallery/gallery.dart
  63. 58 25
      lib/ui/viewer/gallery/gallery_app_bar_widget.dart
  64. 1 1
      lib/ui/viewer/gallery/hooks/add_photos_sheet.dart
  65. 36 11
      lib/ui/viewer/location/location_screen.dart
  66. 44 10
      lib/ui/viewer/search/result/search_result_page.dart
  67. 0 1
      lib/utils/debouncer.dart
  68. 8 5
      lib/utils/delete_file_util.dart
  69. 54 32
      lib/utils/file_uploader.dart
  70. 37 7
      lib/utils/file_uploader_util.dart
  71. 3 3
      lib/utils/file_util.dart
  72. 9 7
      lib/utils/magic_util.dart
  73. 6 5
      pubspec.lock
  74. 5 3
      pubspec.yaml

+ 1 - 1
analysis_options.yaml

@@ -61,7 +61,7 @@ analyzer:
     cancel_subscriptions: error
 
 
-    unawaited_futures: ignore # convert to warning after fixing existing issues
+    unawaited_futures: warning # convert to warning after fixing existing issues
     invalid_dependency: info
     use_build_context_synchronously: ignore # experimental lint, requires many changes
     prefer_interpolation_to_compose_strings: ignore # later too many warnings

+ 36 - 0
fastlane/metadata/android/de/full_description.txt

@@ -0,0 +1,36 @@
+ente ist eine einfache App, um Ihre Fotos und Videos automatisch zu sichern und zu organisieren.
+
+Wenn Sie auf der Suche nach einer privaten Alternative sind, um Ihre Erinnerungen zu bewahren, sind Sie an der richtigen Stelle. Mit Ente werden sie Ende-zu-Ende-verschlüsselt gespeichert (e2ee). Dies bedeutet, dass nur Sie sie sehen können.
+
+Wir haben Open-Source Apps auf allen Plattformen, und Ihre Fotos werden nahtlos zwischen all Ihren Geräten verschlüsselt (e2ee) synchronisiert.
+
+ente ermöglicht es deine Alben simpel & schnell mit deinen Geliebten zu teilen. Die müssen nicht mal ente haben. Du kannst öffentlich einsehbare Links teilen, wo sie dein Album sehen und zusammenarbeiten können, indem sie Fotos hinzufügen, sogar ohne einen Account oder eine App.
+
+Ihre verschlüsselten Daten werden zu 3 verschiedenen Orten repliziert, unter anderem zu einem Schutzbunker in Paris. Wir nehmen die Erhaltung der Nachwelt ernst und machen es Dir leicht, dafür zu sorgen, dass Deine Erinnerungen Dich überdauern.
+
+Wir sind hier, um die sicherste Foto-App aller Zeiten zu entwickeln, begleite uns auf unserem Weg!
+
+FEATURES
+- Sicherungen in Originalqualität, jeder Pixel ist wichtig
+- Familienpläne, damit Du den Speicher mit Deiner Familie teilen kannst
+- Kollaborative Alben, sodass du nach einer Reise Fotos zusammenstellen kannst
+- Geteilte Ordner für den Fall, dass Dein Partner Deine "Kamera" Klicks genießen soll
+- Links zu einem Album, welche mit einem Passwort geschützt werden können
+- Möglichkeit Speicherplatz freizugeben, indem bereits gesicherte Daten auf dem Gerät entfernt werden
+- Menschliche Unterstützung, denn Sie sind es wert
+- Beschreibungen, damit Sie Ihre Erinnerungen beschriften und leicht wiederfinden können
+- Integrierte Bildbearbeitung, um den letzten Schliff zu geben
+- Favorisiere, verstecke und erlebe deine Erinnerungen, denn sie sind kostbar
+- Importieren Sie mit einem Klick von Google, Apple, Ihrer Festplatte und mehr
+- Dunkles Theme, weil Ihre Fotos darin gut aussehen
+- 2FA, 3FA, biometrische Authentifizierung
+- und noch VIELES mehr!
+
+BERECHTIGUNGEN
+ente benötigt bestimmte Berechtigungen um als Anbieter eines Fotospeichers fungieren zu können. Diese können unter folgendem Link betrachtet werden: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md
+
+PREIS
+Wir bieten keine lebenslang kostenlosen Abonnements an, da es für uns wichtig ist, einen nachhaltigen Service anzubieten. Wir bieten jedoch bezahlbare Abonemments an, welche auch mit der Familie geteilt werden können. Mehr Informationen sind auf ente.io zu finden.
+
+SUPPORT
+Wir sind stolz darauf einen persönlichen Support anzubieten. Falls Sie ein Abonnement besitzen, können Sie sich mit Ihrem Anliegen via E-Mail an team@ente.io wenden und erhalten eine Antwort innerhalb von 24 Stunden.

+ 1 - 0
fastlane/metadata/android/pl/short_description.txt

@@ -0,0 +1 @@
+ente to w pełni szyfrowana aplikacja do przechowywania zdjęć

+ 1 - 0
fastlane/metadata/android/pl/title.txt

@@ -0,0 +1 @@
+ente - szyfrowane przechowywanie zdjęć

+ 33 - 0
fastlane/metadata/ios/pl/description.txt

@@ -0,0 +1,33 @@
+Ente to prosta aplikacja do automatycznego tworzenia kopii zapasowych oraz porządkowania zdjęć i filmów.
+
+Jeśli szukasz przyjaznej dla prywatności alternatywy, aby zachować swoje wspomnienia, jesteś we właściwym miejscu. Dzięki Ente są one przechowywane w pełni zaszyfrowane (e2ee). Oznacza to, że tylko Ty możesz je oglądać.
+
+Mamy aplikacje na wszystkich platformach, a Twoje zdjęcia będą płynnie synchronizowane między Twoimi urządzeniami w sposób szyfrowany (e2ee).
+
+Ente ułatwia również udostępnianie albumów najbliższym. Możesz udostępniać je bezpośrednio innym użytkownikom Ente, w pełni zaszyfrowane; lub za pomocą publicznie dostępnych linków.
+
+Twoje zaszyfrowane dane są przechowywane w wielu lokalizacjach, między innymi w schronie przeciwatomowym w Paryżu. Poważnie podchodzimy do kwestii zachowania pamięci i ułatwiamy zadbanie o to, by wspomnienia przetrwały dłużej niż ty sam.
+
+Naszym celem jest stworzenie najbezpieczniejszej aplikacji do zdjęć, dołącz do nas!
+
+FUNKCJE
+- Oryginalne kopie zapasowe wysokiej jakości, ponieważ każdy piksel jest ważny
+- Plany rodzinne, umożliwiające współdzielenie pamięci z rodziną
+- Współdzielone foldery, na wypadek, gdybyś chciał, aby Twój partner mógł cieszyć się Twoimi kliknięciami "Aparatu"
+- Linki do albumów, które mogą być chronione hasłem i automatycznie wygasać
+- Możliwość zwolnienia miejsca poprzez usunięcie plików, które zostały bezpiecznie zarchiwizowane
+- Edytor obrazów, aby dodać ostatnie poprawki
+- Ulubione, ukryj i przeżyj swoje wspomnienia, ponieważ są cenne
+- Import jednym kliknięciem ze wszystkich głównych dostawców pamięci masowej
+- Ciemny motyw, ponieważ Twoje zdjęcia wyglądają w nim dobrze
+- 2FA, 3FA, uwierzytelnianie biometryczne
+- i o WIELE więcej!
+
+WARUNKI
+Nie oferujemy planów darmowych na zawsze, ponieważ ważne jest dla nas, abyśmy pozostali zrównoważeni i wytrzymali próbę czasu. Zamiast tego oferujemy przystępne cenowo plany, którymi można swobodnie dzielić się z rodziną. Więcej informacji możesz znaleźć na stronie ente.io.
+
+WSPARCIE
+Jesteśmy dumni z tego, że oferujemy ludzkie wsparcie. Jeśli jesteś naszym płatnym klientem, możesz skontaktować się z nami pod adresem team@ente.io i oczekiwać odpowiedzi od naszego zespołu w ciągu 24 godzin.
+
+CENA
+https://ente.io/terms

+ 1 - 0
fastlane/metadata/ios/pl/keywords.txt

@@ -0,0 +1 @@
+zdjęcia,fotografia,rodzina,prywatność,chmura,kopia zapasowa,filmy,zdjęcie,szyfrowanie,przechowywanie,album,alternatywa

+ 1 - 0
fastlane/metadata/ios/pl/name.txt

@@ -0,0 +1 @@
+ente Zdjęcia

+ 1 - 0
fastlane/metadata/ios/pl/subtitle.txt

@@ -0,0 +1 @@
+Szyfrowane miejsce na zdjęcia

+ 1 - 0
fastlane/metadata/playstore/pl/short_description.txt

@@ -0,0 +1 @@
+Szyfrowane przechowywanie zdjęć - twórz kopie zapasowe, organizuj i udostępniaj swoje zdjęcia i filmy

+ 1 - 0
fastlane/metadata/playstore/pl/title.txt

@@ -0,0 +1 @@
+ente Zdjęcia

+ 1 - 1
lib/core/configuration.dart

@@ -173,7 +173,7 @@ class Configuration {
       SearchService.instance.clearCache();
       Bus.instance.fire(UserLoggedOutEvent());
     } else {
-      _preferences.setBool("auto_logout", true);
+      await _preferences.setBool("auto_logout", true);
     }
   }
 

+ 3 - 3
lib/core/error-reporting/super_logging.dart

@@ -171,7 +171,7 @@ class SuperLogging {
       await setupLogDir();
     }
     if (sentryIsEnabled) {
-      setupSentry();
+      setupSentry().ignore();
     }
 
     Logger.root.level = Level.ALL;
@@ -266,7 +266,7 @@ class SuperLogging {
 
     // add error to sentry queue
     if (sentryIsEnabled && rec.error != null) {
-      _sendErrorToSentry(rec.error!, null);
+      _sendErrorToSentry(rec.error!, null).ignore();
     }
   }
 
@@ -303,7 +303,7 @@ class SuperLogging {
   static Future<void> setupSentry() async {
     await for (final error in sentryQueueControl.stream.asBroadcastStream()) {
       try {
-        Sentry.captureException(
+        await Sentry.captureException(
           error,
         );
       } catch (e) {

+ 1 - 0
lib/core/errors.dart

@@ -6,6 +6,7 @@ enum InvalidReason {
   imageToLivePhotoTypeChanged,
   livePhotoVideoMissing,
   thumbnailMissing,
+  tooLargeFile,
   unknown,
 }
 

+ 13 - 0
lib/db/file_updation_db.dart

@@ -14,6 +14,7 @@ class FileUpdationDB {
   static const tableName = 're_upload_tracker';
   static const columnLocalID = 'local_id';
   static const columnReason = 'reason';
+  static const livePhotoCheck = 'livePhotoCheck';
 
   static const modificationTimeUpdated = 'modificationTimeUpdated';
 
@@ -127,6 +128,18 @@ class FileUpdationDB {
     );
   }
 
+  // check if entry existing for given localID and reason
+  Future<bool> isExisting(String localID, String reason) async {
+    final db = await instance.database;
+    final String whereClause =
+        '$columnLocalID = "$localID" AND $columnReason = "$reason"';
+    final rows = await db.query(
+      tableName,
+      where: whereClause,
+    );
+    return rows.isNotEmpty;
+  }
+
   Future<List<String>> getLocalIDsForPotentialReUpload(
     int limit,
     String reason,

+ 18 - 0
lib/db/files_db.dart

@@ -1466,6 +1466,24 @@ class FilesDB {
     return result;
   }
 
+  // For a given userID, return unique localID for all uploaded live photos
+  Future<List<String>> getLivePhotosForUser(int userId) async {
+    final db = await instance.database;
+    final rows = await db.query(
+      filesTable,
+      columns: [columnLocalID],
+      distinct: true,
+      where: '$columnOwnerID = ? AND '
+          '$columnFileType = ? AND $columnLocalID IS NOT NULL',
+      whereArgs: [userId, getInt(FileType.livePhoto)],
+    );
+    final result = <String>[];
+    for (final row in rows) {
+      result.add(row[columnLocalID] as String);
+    }
+    return result;
+  }
+
   // updateSizeForUploadIDs takes a map of upploadedFileID and fileSize and
   // update the fileSize for the given uploadedFileID
   Future<void> updateSizeForUploadIDs(

+ 1 - 1
lib/generated/l10n.dart

@@ -6370,7 +6370,7 @@ class S {
   }
 
   /// `{completed}/{total} memories preserved`
-  String syncProgress(int completed, int total) {
+  String syncProgress(String completed, String total) {
     return Intl.message(
       '$completed/$total memories preserved',
       name: 'syncProgress',

+ 60 - 8
lib/l10n/intl_de.arb

@@ -557,6 +557,8 @@
   "faqs": "FAQs",
   "renewsOn": "Erneuert am {endDate}",
   "freeTrialValidTill": "Kostenlose Demo verfügbar bis zum {endDate}",
+  "validTill": "Gültig bis {endDate}",
+  "addOnValidTill": "Dein {storageAmount} Add-on ist gültig bis {endDate}",
   "playStoreFreeTrialValidTill": "Kostenlose Testversion gültig bis {endDate}.\nSie können anschließend ein bezahltes Paket auswählen.",
   "subWillBeCancelledOn": "Ihr Abo endet am {endDate}",
   "subscription": "Abonnement",
@@ -898,10 +900,10 @@
     "description": "Text to tell user how many memories have been preserved",
     "placeholders": {
       "completed": {
-        "type": "int"
+        "type": "String"
       },
       "total": {
-        "type": "int"
+        "type": "String"
       }
     }
   },
@@ -927,6 +929,8 @@
   "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Etwas ist schiefgelaufen. Bitte versuche es später noch einmal. Sollte der Fehler weiter bestehen, kontaktiere unser Supportteam.",
   "error": "Fehler",
   "tempErrorContactSupportIfPersists": "Etwas ist schiefgelaufen. Bitte versuche es später noch einmal. Sollte der Fehler weiter bestehen, kontaktiere unser Supportteam.",
+  "networkHostLookUpErr": "Ente ist im Moment nicht erreichbar. Bitte überprüfen Sie Ihre Netzwerkeinstellungen. Sollte das Problem bestehen bleiben, wenden Sie sich bitte an den Support.",
+  "networkConnectionRefusedErr": "Ente ist im Moment nicht erreichbar. Bitte versuchen Sie es später erneut. Sollte das Problem bestehen bleiben, wenden Sie sich bitte an den Support.",
   "cachedData": "Daten im Cache",
   "clearCaches": "Cache löschen",
   "remoteImages": "Grafiken aus externen Quellen",
@@ -955,12 +959,22 @@
   "loadMessage7": "Unsere mobilen Apps laufen im Hintergrund, um neue Fotos zu verschlüsseln und zu sichern",
   "loadMessage8": "web.ente.io hat einen Spitzen-Uploader",
   "loadMessage9": "Wir verwenden Xchacha20Poly1305, um Ihre Daten sicher zu verschlüsseln",
+  "photoDescriptions": "Foto Beschreibungen",
+  "fileTypesAndNames": "Dateitypen und -namen",
+  "location": "Standort",
+  "moments": "Momente",
+  "searchFaceEmptySection": "Finde alle Foto von einer Person",
+  "searchDatesEmptySection": "Suche nach Datum, Monat oder Jahr",
+  "searchLocationEmptySection": "Gruppiere Fotos, die innerhalb des Radius eines bestimmten Fotos aufgenommen wurden",
+  "searchPeopleEmptySection": "Laden Sie Personen ein, damit Sie geteilte Fotos hier einsehen können",
+  "searchAlbumsEmptySection": "Alben",
+  "searchFileTypesAndNamesEmptySection": "Dateitypen und -namen",
+  "searchCaptionEmptySection": "Füge Beschreibungen wie \"#trip\" in der Fotoinfo hinzu um diese schnell hier wiederzufinden",
   "language": "Sprache",
   "selectLanguage": "Sprache auswählen",
   "locationName": "Standortname",
   "addLocation": "Ort hinzufügen",
   "groupNearbyPhotos": "Fotos in der Nähe gruppieren",
-  "location": "Standort",
   "kiloMeterUnit": "km",
   "addLocationButton": "Hinzufügen",
   "radius": "Umkreis",
@@ -1102,9 +1116,47 @@
   "crashReporting": "Absturzbericht",
   "addToHiddenAlbum": "Zum versteckten Album hinzufügen",
   "moveToHiddenAlbum": "Zu verstecktem Album verschieben",
-  "fileTypes": "File types",
-  "deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
-  "yourMap": "Your map",
-  "modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for",
-  "contacts": "Contacts"
+  "fileTypes": "Dateitypen",
+  "deleteConfirmDialogBody": "Dieses Konto ist mit anderen ente Apps verknüpft, sofern du diese benutzt.\\n\\nDeine hochgeladenen Daten werden zur permanenten Löschung freigegeben. Dies gilt für alle ente Apps.",
+  "hearUsWhereTitle": "Wie hast du von Ente erfahren? (optional)",
+  "hearUsExplanation": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!",
+  "viewAddOnButton": "Zeige Add-ons",
+  "addOns": "Add-ons",
+  "addOnPageSubtitle": "Details der Add-ons",
+  "yourMap": "Deine Karte",
+  "modifyYourQueryOrTrySearchingFor": "Ändere deine Suchanfrage oder suche nach",
+  "blackFridaySale": "Black-Friday-Aktion",
+  "upto50OffUntil4thDec": "Bis zu 50% Rabatt bis zum 4. Dezember.",
+  "photos": "Fotos",
+  "videos": "Videos",
+  "livePhotos": "Live-Fotos",
+  "searchHint1": "Schnell auf dem Gerät suchen",
+  "searchHint2": "Fotodaten, Beschreibungen",
+  "searchHint3": "Alben, Dateinamen und -typen",
+  "searchHint4": "Ort",
+  "searchHint5": "Demnächst: Gesichter & magische Suche ✨",
+  "addYourPhotosNow": "Füge deine Foto jetzt hinzu",
+  "searchResultCount": "{count, plural, one{{count} Ergebnis gefunden} other{{count} Ergebnisse gefunden}}",
+  "@searchResultCount": {
+    "description": "Text to tell user how many results were found for their search query",
+    "placeholders": {
+      "count": {
+        "example": "1|2|3",
+        "type": "int"
+      }
+    }
+  },
+  "faces": "Gesichter",
+  "contents": "Inhalte",
+  "addNew": "Hinzufügen",
+  "@addNew": {
+    "description": "Text to add a new item (location tag, album, caption etc)"
+  },
+  "contacts": "Kontakte",
+  "noInternetConnection": "Keine Internetverbindung",
+  "pleaseCheckYourInternetConnectionAndTryAgain": "Bitte überprüfe deine Internetverbindung und versuche es erneut.",
+  "signOutFromOtherDevices": "Von anderen Geräten abmelden",
+  "signOutOtherBody": "Falls du denkst, dass jemand dein Passwort kennen könnte, kannst du alle anderen Geräte von deinem Account abmelden.",
+  "signOutOtherDevices": "Andere Geräte abmelden",
+  "doNotSignOut": "Melde dich nicht ab"
 }

+ 2 - 2
lib/l10n/intl_en.arb

@@ -907,10 +907,10 @@
     "description": "Text to tell user how many memories have been preserved",
     "placeholders": {
       "completed": {
-        "type": "int"
+        "type": "String"
       },
       "total": {
-        "type": "int"
+        "type": "String"
       }
     }
   },

+ 2 - 2
lib/l10n/intl_nl.arb

@@ -900,10 +900,10 @@
     "description": "Text to tell user how many memories have been preserved",
     "placeholders": {
       "completed": {
-        "type": "int"
+        "type": "String"
       },
       "total": {
-        "type": "int"
+        "type": "String"
       }
     }
   },

+ 2 - 2
lib/l10n/intl_zh.arb

@@ -900,10 +900,10 @@
     "description": "Text to tell user how many memories have been preserved",
     "placeholders": {
       "completed": {
-        "type": "int"
+        "type": "String"
       },
       "total": {
-        "type": "int"
+        "type": "String"
       }
     }
   },

+ 4 - 4
lib/main.dart

@@ -67,8 +67,8 @@ void main() async {
   MediaKit.ensureInitialized();
   final savedThemeMode = await AdaptiveTheme.getThemeMode();
   await _runInForeground(savedThemeMode);
-  BackgroundFetch.registerHeadlessTask(_headlessTaskHandler);
-  FlutterDisplayMode.setHighRefreshRate();
+  unawaited(BackgroundFetch.registerHeadlessTask(_headlessTaskHandler));
+  FlutterDisplayMode.setHighRefreshRate().ignore();
 }
 
 Future<void> _runInForeground(AdaptiveThemeMode? savedThemeMode) async {
@@ -131,7 +131,7 @@ Future<void> _runInBackground(String taskId) async {
     _scheduleSuicide(kBGTaskTimeout, taskId); // To prevent OS from punishing us
   }
   await _init(true, via: 'runViaBackgroundTask');
-  UpdateService.instance.showUpdateNotification();
+  UpdateService.instance.showUpdateNotification().ignore();
   await _sync('bgSync');
   BackgroundFetch.finish(taskId);
 }
@@ -160,7 +160,7 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
     AppLifecycleService.instance.onAppInForeground('init via: $via');
   }
   // Start workers asynchronously. No need to wait for them to start
-  Computer.shared().turnOn(workersCount: 4);
+  Computer.shared().turnOn(workersCount: 4).ignore();
   CryptoUtil.init();
   await ObjectBox.instance.init();
   await NetworkClient.instance.init();

+ 3 - 3
lib/models/search/search_types.dart

@@ -182,7 +182,7 @@ extension SectionTypeExtensions on SectionType {
     switch (this) {
       case SectionType.contacts:
         return () async {
-          shareText(
+          await shareText(
             S.of(context).shareTextRecommendUsingEnte,
           );
         };
@@ -211,7 +211,7 @@ extension SectionTypeExtensions on SectionType {
               try {
                 final Collection c =
                     await CollectionsService.instance.createAlbum(text);
-                routeToPage(
+                await routeToPage(
                   context,
                   CollectionPage(CollectionWithThumbnail(c, null)),
                 );
@@ -223,7 +223,7 @@ extension SectionTypeExtensions on SectionType {
             },
           );
           if (result is Exception) {
-            showGenericErrorDialog(context: context, error: result);
+            await showGenericErrorDialog(context: context, error: result);
           }
         };
       default:

+ 2 - 2
lib/services/billing_service.dart

@@ -184,7 +184,7 @@ class BillingService {
     try {
       final String jwtToken = await UserService.instance.getFamiliesToken();
       final bool familyExist = userDetails.isPartOfFamily();
-      Navigator.of(context).push(
+      await Navigator.of(context).push(
         MaterialPageRoute(
           builder: (BuildContext context) {
             return WebPage(
@@ -196,7 +196,7 @@ class BillingService {
       );
     } catch (e) {
       await dialog.hide();
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
     }
     await dialog.hide();
   }

+ 1 - 1
lib/services/entity_service.dart

@@ -162,7 +162,7 @@ class EntityService {
         encryptedKey = response.encryptedKey;
         header = response.header;
         await _prefs.setString(_getEntityKeyPrefix(type), encryptedKey);
-        _prefs.setString(_getEntityHeaderPrefix(type), header);
+        await _prefs.setString(_getEntityHeaderPrefix(type), header);
       }
       final entityKey = CryptoUtil.decryptSync(
         CryptoUtil.base642bin(encryptedKey),

+ 9 - 5
lib/services/files_service.dart

@@ -49,11 +49,7 @@ class FilesService {
       if (uploadIDsWithMissingSize.isEmpty) {
         return Future.value(true);
       }
-      final batchedFiles = uploadIDsWithMissingSize.chunks(1000);
-      for (final batch in batchedFiles) {
-        final Map<int, int> uploadIdToSize = await getFilesSizeFromInfo(batch);
-        await _filesDB.updateSizeForUploadIDs(uploadIdToSize);
-      }
+      await backFillSizes(uploadIDsWithMissingSize);
       return Future.value(true);
     } catch (e, s) {
       _logger.severe("error during has migrated sizes", e, s);
@@ -61,6 +57,14 @@ class FilesService {
     }
   }
 
+  Future<void> backFillSizes(List<int> uploadIDsWithMissingSize) async {
+    final batchedFiles = uploadIDsWithMissingSize.chunks(1000);
+    for (final batch in batchedFiles) {
+      final Map<int, int> uploadIdToSize = await getFilesSizeFromInfo(batch);
+      await _filesDB.updateSizeForUploadIDs(uploadIdToSize);
+    }
+  }
+
   Future<Map<int, int>> getFilesSizeFromInfo(List<int> uploadedFileID) async {
     try {
       final response = await _enteDio.post(

+ 1 - 1
lib/services/hidden_service.dart

@@ -154,7 +154,7 @@ extension HiddenService on CollectionsService {
     } catch (e, s) {
       _logger.severe("Could not hide", e, s);
       await dialog.hide();
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
       return false;
     } finally {
       await dialog.hide();

+ 9 - 5
lib/services/local_authentication_service.dart

@@ -1,3 +1,5 @@
+import "dart:async";
+
 import 'package:flutter/material.dart';
 import 'package:local_auth/local_auth.dart';
 import 'package:photos/core/configuration.dart';
@@ -22,7 +24,7 @@ class LocalAuthenticationService {
         Configuration.instance.shouldShowLockScreen(),
       );
       if (!result) {
-        showToast(context, infoMessage);
+        unawaited(showToast(context, infoMessage));
         return false;
       } else {
         return true;
@@ -54,10 +56,12 @@ class LocalAuthenticationService {
             .setEnabled(Configuration.instance.shouldShowLockScreen());
       }
     } else {
-      showErrorDialog(
-        context,
-        errorDialogTitle,
-        errorDialogContent,
+      unawaited(
+        showErrorDialog(
+          context,
+          errorDialogTitle,
+          errorDialogContent,
+        ),
       );
     }
     return false;

+ 205 - 5
lib/services/local_file_update_service.dart

@@ -7,8 +7,12 @@ import "package:photos/core/configuration.dart";
 import 'package:photos/core/errors.dart';
 import 'package:photos/db/file_updation_db.dart';
 import 'package:photos/db/files_db.dart';
+import "package:photos/extensions/list.dart";
+import "package:photos/extensions/stop_watch.dart";
+import "package:photos/models/file/extensions/file_props.dart";
 import 'package:photos/models/file/file.dart';
 import 'package:photos/models/file/file_type.dart';
+import "package:photos/services/files_service.dart";
 import 'package:photos/utils/file_uploader_util.dart';
 import 'package:photos/utils/file_util.dart';
 import 'package:shared_preferences/shared_preferences.dart';
@@ -19,6 +23,9 @@ class LocalFileUpdateService {
   late FileUpdationDB _fileUpdationDB;
   late SharedPreferences _prefs;
   late Logger _logger;
+  final String _iosLivePhotoSizeMigrationDone = 'fm_ios_live_photo_check';
+  final String _doneLivePhotoImport = 'fm_import_ios_live_photo_check';
+  static int twoHundredKb = 200 * 1024;
   final List<String> _oldMigrationKeys = [
     'fm_badCreationTime',
     'fm_badCreationTimeCompleted',
@@ -26,6 +33,8 @@ class LocalFileUpdateService {
     'fm_missingLocationV2MigrationDone',
     'fm_badLocationImportDone',
     'fm_badLocationMigrationDone',
+    'fm_ios_live_photo_size',
+    'fm_import_ios_live_photo_size',
   ];
 
   Completer<void>? _existingMigration;
@@ -50,8 +59,9 @@ class LocalFileUpdateService {
     _existingMigration = Completer<void>();
     try {
       await _markFilesWhichAreActuallyUpdated();
-      if (Platform.isAndroid) {
-        _cleanUpOlderMigration().ignore();
+      _cleanUpOlderMigration().ignore();
+      if (!Platform.isAndroid) {
+        await _handleLivePhotosSizedCheck();
       }
     } catch (e, s) {
       _logger.severe('failed to perform migration', e, s);
@@ -71,15 +81,16 @@ class LocalFileUpdateService {
       }
     }
     if (hasOldMigrationKey) {
-      for (var element in _oldMigrationKeys) {
-        _prefs.remove(element);
-      }
       await _fileUpdationDB.deleteByReasons([
         'missing_location',
         'badCreationTime',
         'missingLocationV2',
         'badLocationCord',
+        'livePhotoSize',
       ]);
+      for (var element in _oldMigrationKeys) {
+        await _prefs.remove(element);
+      }
     }
   }
 
@@ -199,6 +210,181 @@ class LocalFileUpdateService {
     );
   }
 
+  Future<void> checkLivePhoto(EnteFile file) async {
+    if (file.localID == null ||
+        file.localID!.isEmpty ||
+        !file.isUploaded ||
+        file.fileType != FileType.livePhoto ||
+        !file.isOwner) {
+      return;
+    }
+    if (_prefs.containsKey(_iosLivePhotoSizeMigrationDone)) {
+      return;
+    }
+    bool hasEntry = await _fileUpdationDB.isExisting(
+      file.localID!,
+      FileUpdationDB.livePhotoCheck,
+    );
+    if (hasEntry) {
+      _logger.info('eager checkLivePhoto ${file.tag}');
+      await _checkLivePhotoWithLowOrUnknownSize([file.localID!]);
+    }
+  }
+
+  Future<void> _handleLivePhotosSizedCheck() async {
+    try {
+      if (_prefs.containsKey(_iosLivePhotoSizeMigrationDone)) {
+        return;
+      }
+      await _importLivePhotoReUploadCandidates();
+
+      // singleRunLimit indicates number of files to check during single
+      // invocation of this method. The limit act as a crude way to limit the
+      // resource consumed by the method
+      const int singleRunLimit = 500;
+      final localIDsToProcess =
+          await _fileUpdationDB.getLocalIDsForPotentialReUpload(
+        singleRunLimit,
+        FileUpdationDB.livePhotoCheck,
+      );
+      if (localIDsToProcess.isNotEmpty) {
+        final chunksOf50 = localIDsToProcess.chunks(50);
+        for (final chunk in chunksOf50) {
+          final sTime = DateTime.now().microsecondsSinceEpoch;
+          final List<Future> futures = [];
+          final chunkOf10 = chunk.chunks(10);
+          for (final smallChunk in chunkOf10) {
+            futures.add(_checkLivePhotoWithLowOrUnknownSize(smallChunk));
+          }
+          await Future.wait(futures);
+          final eTime = DateTime.now().microsecondsSinceEpoch;
+          final d = Duration(microseconds: eTime - sTime);
+          _logger.info(
+            'Performed hashCheck for ${chunk.length} livePhoto files '
+            'completed in ${d.inSeconds.toString()} secs',
+          );
+        }
+      } else {
+        await _prefs.setBool(_iosLivePhotoSizeMigrationDone, true);
+      }
+    } catch (e, s) {
+      _logger.severe('error while checking livePhotoSize check', e, s);
+    }
+  }
+
+  Future<void> _checkLivePhotoWithLowOrUnknownSize(
+    List<String> localIDsToProcess,
+  ) async {
+    final int userID = Configuration.instance.getUserID()!;
+    final List<EnteFile> result =
+        await FilesDB.instance.getLocalFiles(localIDsToProcess);
+    final List<EnteFile> localFilesForUser = [];
+    final Set<String> localIDsWithFile = {};
+    final Set<int> missingSizeIDs = {};
+    final Set<String> processedIDs = {};
+    for (EnteFile file in result) {
+      if (file.ownerID == null || file.ownerID == userID) {
+        localFilesForUser.add(file);
+        localIDsWithFile.add(file.localID!);
+        if (file.isUploaded && file.fileSize == null) {
+          missingSizeIDs.add(file.uploadedFileID!);
+        }
+        if (file.isUploaded && file.updationTime == null) {
+          // file already queued for re-upload
+          processedIDs.add(file.localID!);
+        }
+      }
+    }
+    if (missingSizeIDs.isNotEmpty) {
+      await FilesService.instance.backFillSizes(missingSizeIDs.toList());
+      _logger.info('sizes back fill for ${missingSizeIDs.length} files');
+      // return early, let the check run in the next batch
+      return;
+    }
+
+    // if a file for localID doesn't exist, then mark it as processed
+    // otherwise the app will be stuck in retrying same set of ids
+
+    for (String localID in localIDsToProcess) {
+      if (!localIDsWithFile.contains(localID)) {
+        processedIDs.add(localID);
+      }
+    }
+    _logger.info(" check ${localIDsToProcess.length} files for livePhotoSize, "
+        "missing file cnt ${processedIDs.length}");
+
+    for (EnteFile file in localFilesForUser) {
+      if (file.fileSize == null) {
+        _logger.info('fileSize still null, skip this file');
+        continue;
+      } else if (file.fileType != FileType.livePhoto) {
+        _logger.severe('fileType is not livePhoto, skip this file');
+        processedIDs.add(file.localID!);
+        continue;
+      }
+      if (processedIDs.contains(file.localID)) {
+        continue;
+      }
+      try {
+        late MediaUploadData uploadData;
+        late int mediaUploadSize;
+        (uploadData, mediaUploadSize) = await getUploadDataWithSizeSize(file);
+        if ((file.fileSize! - mediaUploadSize).abs() > twoHundredKb) {
+          _logger.info(
+            'Re-upload livePhoto localHash ${uploadData.hashData?.fileHash ?? "null"} & localSize: $mediaUploadSize'
+            ' and remoteHash ${file.hash ?? "null"} & removeSize: ${file.fileSize!}',
+          );
+          await FilesDB.instance.markFilesForReUpload(
+            userID,
+            file.localID!,
+            file.title,
+            file.location,
+            file.creationTime!,
+            file.modificationTime!,
+            file.fileType,
+          );
+        }
+        processedIDs.add(file.localID!);
+      } on InvalidFileError catch (e) {
+        if (e.reason == InvalidReason.livePhotoToImageTypeChanged ||
+            e.reason == InvalidReason.imageToLivePhotoTypeChanged) {
+          // let existing file update check handle this case
+          _fileUpdationDB.insertMultiple(
+            [file.localID!],
+            FileUpdationDB.modificationTimeUpdated,
+          ).ignore();
+        } else {
+          _logger.severe("livePhoto check failed: invalid file ${file.tag}", e);
+        }
+        processedIDs.add(file.localID!);
+      } catch (e) {
+        _logger.severe("livePhoto check failed", e);
+      } finally {}
+    }
+    _logger.info('completed check for ${localIDsToProcess.length} files');
+    await _fileUpdationDB.deleteByLocalIDs(
+      processedIDs.toList(),
+      FileUpdationDB.livePhotoCheck,
+    );
+  }
+
+  Future<void> _importLivePhotoReUploadCandidates() async {
+    if (_prefs.containsKey(_doneLivePhotoImport)) {
+      return;
+    }
+    _logger.info('_importLivePhotoReUploadCandidates');
+    final EnteWatch watch = EnteWatch("_importLivePhotoReUploadCandidates");
+    final int ownerID = Configuration.instance.getUserID()!;
+    final List<String> localIDs =
+        await FilesDB.instance.getLivePhotosForUser(ownerID);
+    await _fileUpdationDB.insertMultiple(
+      localIDs,
+      FileUpdationDB.livePhotoCheck,
+    );
+    watch.log("imported ${localIDs.length} files");
+    await _prefs.setBool(_doneLivePhotoImport, true);
+  }
+
   Future<MediaUploadData> getUploadData(EnteFile file) async {
     final mediaUploadData = await getUploadDataFromEnteFile(file);
     // delete the file from app's internal cache if it was copied to app
@@ -209,4 +395,18 @@ class LocalFileUpdateService {
     }
     return mediaUploadData;
   }
+
+  Future<(MediaUploadData, int)> getUploadDataWithSizeSize(
+    EnteFile file,
+  ) async {
+    final mediaUploadData = await getUploadDataFromEnteFile(file);
+    final int size = await mediaUploadData.sourceFile!.length();
+    // delete the file from app's internal cache if it was copied to app
+    // for upload. Shared Media should only be cleared when the upload
+    // succeeds.
+    if (Platform.isIOS && mediaUploadData.sourceFile != null) {
+      await mediaUploadData.sourceFile?.delete();
+    }
+    return (mediaUploadData, size);
+  }
 }

+ 35 - 3
lib/services/remote_sync_service.dart

@@ -46,6 +46,7 @@ class RemoteSyncService {
   final LocalFileUpdateService _localFileUpdateService =
       LocalFileUpdateService.instance;
   int _completedUploads = 0;
+  int _ignoredUploads = 0;
   late SharedPreferences _prefs;
   Completer<void>? _existingSync;
   bool _isExistingSyncSilent = false;
@@ -123,7 +124,13 @@ class RemoteSyncService {
       }
       final filesToBeUploaded = await _getFilesToBeUploaded();
       final hasUploadedFiles = await _uploadFiles(filesToBeUploaded);
-      _logger.info("File upload complete");
+      if (filesToBeUploaded.isNotEmpty) {
+        _logger.info(
+            "Files ${filesToBeUploaded.length} queued for upload, completed: "
+            "$_completedUploads, ignored $_ignoredUploads");
+      } else {
+        _logger.info("No files to upload for this session");
+      }
       if (hasUploadedFiles) {
         await _pullDiff();
         _existingSync?.complete();
@@ -145,6 +152,11 @@ class RemoteSyncService {
         if (filesToBeUploaded.isEmpty) {
           await _uploader.removeStaleFiles();
         }
+        if (_ignoredUploads > 0) {
+          _logger.info("Ignored $_ignoredUploads files for upload, fire "
+              "backup done");
+          Bus.instance.fire(SyncStatusUpdate(SyncStatus.completedBackup));
+        }
         _existingSync?.complete();
         _existingSync = null;
       }
@@ -157,7 +169,7 @@ class RemoteSyncService {
           e is WiFiUnavailableError ||
           e is StorageLimitExceededError ||
           e is SyncStopRequestedError) {
-        _logger.warning("Error executing remote sync", e);
+        _logger.warning("Error executing remote sync", e, s);
         rethrow;
       } else {
         _logger.severe("Error executing remote sync ", e, s);
@@ -537,6 +549,7 @@ class RemoteSyncService {
     }
 
     _completedUploads = 0;
+    _ignoredUploads = 0;
     final int toBeUploaded = filesToBeUploaded.length + updatedFileIDs.length;
     if (toBeUploaded > 0) {
       Bus.instance.fire(SyncStatusUpdate(SyncStatus.preparingForUpload));
@@ -616,7 +629,10 @@ class RemoteSyncService {
   void _uploadFile(EnteFile file, int collectionID, List<Future> futures) {
     final future = _uploader
         .upload(file, collectionID)
-        .then((uploadedFile) => _onFileUploaded(uploadedFile));
+        .then((uploadedFile) => _onFileUploaded(uploadedFile))
+        .onError(
+          (error, stackTrace) => _onFileUploadError(error, stackTrace, file),
+        );
     futures.add(future);
   }
 
@@ -650,6 +666,22 @@ class RemoteSyncService {
     );
   }
 
+  void _onFileUploadError(
+    Object? error,
+    StackTrace stackTrace,
+    EnteFile file,
+  ) {
+    if (error == null) {
+      return;
+    }
+    if (error is InvalidFileError) {
+      _ignoredUploads++;
+      _logger.warning("Invalid file error", error);
+    } else {
+      throw error;
+    }
+  }
+
   /* _storeDiff maps each remoteFile to existing
       entries in files table. When match is found, it compares both file to
       perform relevant actions like

+ 9 - 8
lib/services/user_service.dart

@@ -338,7 +338,7 @@ class UserService {
         Widget page;
         final String twoFASessionID = response.data["twoFactorSessionID"];
         if (twoFASessionID.isNotEmpty) {
-          setTwoFactor(value: true);
+          await setTwoFactor(value: true);
           page = TwoFactorAuthenticationPage(twoFASessionID);
         } else {
           await _saveConfiguration(response);
@@ -354,7 +354,7 @@ class UserService {
             );
           }
         }
-        Navigator.of(context).pushAndRemoveUntil(
+        await Navigator.of(context).pushAndRemoveUntil(
           MaterialPageRoute(
             builder: (BuildContext context) {
               return page;
@@ -740,9 +740,10 @@ class UserService {
       );
       await dialog.hide();
       if (response.statusCode == 200) {
-        showShortToast(context, S.of(context).authenticationSuccessful);
+        showShortToast(context, S.of(context).authenticationSuccessful)
+            .ignore();
         await _saveConfiguration(response);
-        Navigator.of(context).pushAndRemoveUntil(
+        await Navigator.of(context).pushAndRemoveUntil(
           MaterialPageRoute(
             builder: (BuildContext context) {
               return const PasswordReentryPage();
@@ -755,8 +756,8 @@ class UserService {
       await dialog.hide();
       _logger.severe(e);
       if (e.response != null && e.response!.statusCode == 404) {
-        showToast(context, "Session expired");
-        Navigator.of(context).pushAndRemoveUntil(
+        showToast(context, "Session expired").ignore();
+        await Navigator.of(context).pushAndRemoveUntil(
           MaterialPageRoute(
             builder: (BuildContext context) {
               return const LoginPage();
@@ -809,7 +810,7 @@ class UserService {
     } on DioError catch (e) {
       _logger.severe(e);
       if (e.response != null && e.response!.statusCode == 404) {
-        showToast(context, S.of(context).sessionExpired);
+        showToast(context, S.of(context).sessionExpired).ignore();
         Navigator.of(context).pushAndRemoveUntil(
           MaterialPageRoute(
             builder: (BuildContext context) {
@@ -959,7 +960,7 @@ class UserService {
     try {
       recoveryKey = await getOrCreateRecoveryKey(context);
     } catch (e) {
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
       return false;
     }
     final dialog = createProgressDialog(context, S.of(context).verifying);

+ 1 - 1
lib/ui/account/delete_account_page.dart

@@ -289,7 +289,7 @@ class _DeleteAccountPageState extends State<DeleteAccountPage> {
       showShortToast(context, S.of(context).yourAccountHasBeenDeleted);
     } catch (e, s) {
       Logger("DeleteAccount").severe("failed to delete", e, s);
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
     }
   }
 

+ 3 - 3
lib/ui/account/password_entry_page.dart

@@ -404,7 +404,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
     } catch (e, s) {
       _logger.severe(e, s);
       await dialog.hide();
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
     }
   }
 
@@ -456,7 +456,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
         } catch (e, s) {
           _logger.severe(e, s);
           await dialog.hide();
-          showGenericErrorDialog(context: context, error: e);
+          await showGenericErrorDialog(context: context, error: e);
         }
       }
 
@@ -481,7 +481,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
           S.of(context).sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease,
         );
       } else {
-        showGenericErrorDialog(context: context, error: e);
+        await showGenericErrorDialog(context: context, error: e);
       }
     }
   }

+ 1 - 1
lib/ui/account/verify_recovery_page.dart

@@ -109,7 +109,7 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
           ),
         );
       } catch (e) {
-        showGenericErrorDialog(context: context, error: e);
+        await showGenericErrorDialog(context: context, error: e);
         return;
       }
     }

+ 5 - 2
lib/ui/actions/collection/collection_file_actions.dart

@@ -73,7 +73,10 @@ extension CollectionFileActions on CollectionActions {
     );
     if (actionResult?.action != null &&
         actionResult!.action == ButtonAction.error) {
-      showGenericErrorDialog(context: bContext, error: actionResult.exception);
+      await showGenericErrorDialog(
+        context: bContext,
+        error: actionResult.exception,
+      );
     } else {
       selectedFiles.clearAll();
     }
@@ -187,7 +190,7 @@ extension CollectionFileActions on CollectionActions {
     } catch (e, s) {
       logger.severe("Failed to add to album", e, s);
       await dialog?.hide();
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
       rethrow;
     }
   }

+ 56 - 11
lib/ui/actions/collection/collection_sharing_actions.dart

@@ -1,3 +1,5 @@
+import "dart:async";
+
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/core/configuration.dart';
@@ -52,7 +54,7 @@ class CollectionActions {
         _showUnSupportedAlert(context);
       } else {
         logger.severe("Failed to update shareUrl collection", e);
-        showGenericErrorDialog(context: context, error: e);
+        await showGenericErrorDialog(context: context, error: e);
       }
       return false;
     }
@@ -93,7 +95,10 @@ class CollectionActions {
     );
     if (actionResult?.action != null) {
       if (actionResult!.action == ButtonAction.error) {
-        showGenericErrorDialog(context: context, error: actionResult.exception);
+        await showGenericErrorDialog(
+          context: context,
+          error: actionResult.exception,
+        );
       }
       return actionResult.action == ButtonAction.first;
     } else {
@@ -142,7 +147,7 @@ class CollectionActions {
       return collection;
     } catch (e, s) {
       dialog.hide();
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
       logger.severe("Failing to create link for selected files", e, s);
     }
     return null;
@@ -183,7 +188,10 @@ class CollectionActions {
     );
     if (actionResult?.action != null) {
       if (actionResult!.action == ButtonAction.error) {
-        showGenericErrorDialog(context: context, error: actionResult.exception);
+        await showGenericErrorDialog(
+          context: context,
+          error: actionResult.exception,
+        );
       }
       return actionResult.action == ButtonAction.first;
     }
@@ -230,7 +238,7 @@ class CollectionActions {
     } catch (e) {
       await dialog?.hide();
       logger.severe("Failed to get public key", e);
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
       return false;
     }
     // getPublicKey can return null when no user is associated with given
@@ -251,8 +259,10 @@ class CollectionActions {
             labelText: S.of(context).sendInvite,
             isInAlert: true,
             onTap: () async {
-              shareText(
-                S.of(context).shareTextRecommendUsingEnte,
+              unawaited(
+                shareText(
+                  S.of(context).shareTextRecommendUsingEnte,
+                ),
               );
             },
           ),
@@ -272,7 +282,7 @@ class CollectionActions {
           _showUnSupportedAlert(context);
         } else {
           logger.severe("failed to share collection", e);
-          showGenericErrorDialog(context: context, error: e);
+          await showGenericErrorDialog(context: context, error: e);
         }
         return false;
       }
@@ -353,7 +363,10 @@ class CollectionActions {
     );
     if (actionResult?.action != null &&
         actionResult!.action == ButtonAction.error) {
-      showGenericErrorDialog(context: bContext, error: actionResult.exception);
+      await showGenericErrorDialog(
+        context: bContext,
+        error: actionResult.exception,
+      );
       return false;
     }
     if ((actionResult?.action != null) &&
@@ -375,6 +388,23 @@ class CollectionActions {
     await collectionsService.trashEmptyCollection(collection);
   }
 
+  Future<void> removeFromUncatIfPresentInOtherAlbum(
+    Collection collection,
+    BuildContext bContext,
+  ) async {
+    try {
+      final List<EnteFile> files =
+          await FilesDB.instance.getAllFilesCollection(collection.id);
+      await moveFilesFromCurrentCollection(bContext, collection, files);
+    } catch (e) {
+      logger.severe("Failed to remove files from uncategorized", e);
+      await showErrorDialogForException(
+        context: bContext,
+        exception: e as Exception,
+      );
+    }
+  }
+
   // _confirmSharedAlbumDeletion should be shown when user tries to delete an
   // album shared with other ente users.
   Future<bool> _confirmSharedAlbumDeletion(
@@ -437,7 +467,9 @@ class CollectionActions {
     }
 
     if (!isCollectionOwner && split.ownedByOtherUsers.isNotEmpty) {
-      showShortToast(context, S.of(context).canOnlyRemoveFilesOwnedByYou);
+      unawaited(
+        showShortToast(context, S.of(context).canOnlyRemoveFilesOwnedByYou),
+      );
       return;
     }
 
@@ -519,7 +551,20 @@ class CollectionActions {
 
     for (MapEntry<int, List<EnteFile>> entry
         in destCollectionToFilesMap.entries) {
-      await collectionsService.move(entry.key, collection.id, entry.value);
+      if (collection.type == CollectionType.uncategorized &&
+          entry.key == collection.id) {
+        // skip moving files to uncategorized collection from uncategorized
+        // this flow is triggered while cleaning up uncategerized collection
+        logger.info(
+          'skipping moving ${entry.value.length} files to uncategorized collection',
+        );
+      } else {
+        await collectionsService.move(
+          entry.key,
+          collection.id,
+          entry.value,
+        );
+      }
     }
   }
 

+ 4 - 1
lib/ui/actions/file/file_actions.dart

@@ -125,7 +125,10 @@ Future<void> showSingleFileDeleteSheet(
   );
   if (actionResult?.action != null &&
       actionResult!.action == ButtonAction.error) {
-    showGenericErrorDialog(context: context, error: actionResult.exception);
+    await showGenericErrorDialog(
+      context: context,
+      error: actionResult.exception,
+    );
   }
 }
 

+ 6 - 6
lib/ui/collections/album/vertical_list.dart

@@ -109,7 +109,7 @@ class AlbumVerticalListWidget extends StatelessWidget {
         textCapitalization: TextCapitalization.words,
       );
       if (result is Exception) {
-        showGenericErrorDialog(
+        await showGenericErrorDialog(
           context: context,
           error: result,
         );
@@ -311,7 +311,7 @@ class AlbumVerticalListWidget extends StatelessWidget {
           );
           return true;
         } catch (e) {
-          showGenericErrorDialog(context: context, error: e);
+          await showGenericErrorDialog(context: context, error: e);
           return false;
         }
       }
@@ -334,7 +334,7 @@ class AlbumVerticalListWidget extends StatelessWidget {
           ),
         );
       } else {
-        showGenericErrorDialog(context: context, error: result);
+        await showGenericErrorDialog(context: context, error: result);
         _logger.severe("Cannot share collections owned by others");
       }
     }
@@ -356,7 +356,7 @@ class AlbumVerticalListWidget extends StatelessWidget {
       showGenericErrorDialog(
         context: context,
         error: Exception("Can not share collection owned by others"),
-      );
+      ).ignore();
       _logger.severe("Cannot share collections owned by others");
     }
     return Future.value(true);
@@ -413,7 +413,7 @@ class AlbumVerticalListWidget extends StatelessWidget {
     } catch (e, s) {
       _logger.severe("Could not move to album", e, s);
       await dialog.hide();
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
       return false;
     }
   }
@@ -442,7 +442,7 @@ class AlbumVerticalListWidget extends StatelessWidget {
     } catch (e, s) {
       _logger.severe("Could not move to album", e, s);
       await dialog.hide();
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
       return false;
     }
   }

+ 1 - 1
lib/ui/collections/new_album_icon.dart

@@ -55,7 +55,7 @@ class NewAlbumIcon extends StatelessWidget {
           },
         );
         if (result is Exception) {
-          showGenericErrorDialog(context: context, error: result);
+          await showGenericErrorDialog(context: context, error: result);
         }
       },
     );

+ 1 - 2
lib/ui/components/bottom_action_bar/bottom_action_bar_widget.dart

@@ -49,13 +49,12 @@ class BottomActionBarWidget extends StatelessWidget {
       child: Column(
         mainAxisSize: MainAxisSize.min,
         children: [
-          const SizedBox(height: 12),
+          const SizedBox(height: 8),
           FileSelectionActionsWidget(
             galleryType,
             selectedFiles,
             collection: collection,
           ),
-          const SizedBox(height: 20),
           const DividerWidget(dividerType: DividerType.bottomBar),
           ActionBarWidget(
             selectedFiles: selectedFiles,

+ 1 - 1
lib/ui/components/buttons/button_widget.dart

@@ -498,7 +498,7 @@ class _ButtonChildWidgetState extends State<ButtonChildWidget> {
       } else if (exception != null) {
         //This is to show the execution was unsuccessful if the dialog is manually
         //closed before the execution completes.
-        showGenericErrorDialog(context: context, error: exception);
+        showGenericErrorDialog(context: context, error: exception).ignore();
       }
     }
   }

+ 6 - 1
lib/ui/home/status_bar_widget.dart

@@ -1,6 +1,7 @@
 import 'dart:async';
 
 import 'package:flutter/material.dart';
+import "package:intl/intl.dart";
 import "package:logging/logging.dart";
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/ente_theme_data.dart';
@@ -229,7 +230,11 @@ class RefreshIndicatorWidget extends StatelessWidget {
       return S.of(context).encryptingBackup;
     }
     if (event!.status == SyncStatus.inProgress) {
-      return S.of(context).syncProgress(event!.completed!, event!.total!);
+      final format = NumberFormat();
+      return S.of(context).syncProgress(
+            format.format(event!.completed!),
+            format.format(event!.total!),
+          );
     }
     if (event!.status == SyncStatus.paused) {
       return event!.reason;

+ 1 - 1
lib/ui/payment/payment_web_page.dart

@@ -190,7 +190,7 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
     } else {
       // should never reach here
       _logger.severe("unexpected status", uri.toString());
-      showGenericErrorDialog(
+      await showGenericErrorDialog(
         context: context,
         error: Exception("expected payment status $paymentStatus"),
       );

+ 1 - 1
lib/ui/payment/store_subscription_page.dart

@@ -528,7 +528,7 @@ class _StoreSubscriptionPageState extends State<StoreSubscriptionPage> {
                     response.notFoundIDs.toString();
                 _logger.severe(errMsg);
                 await _dialog.hide();
-                showGenericErrorDialog(
+                await showGenericErrorDialog(
                   context: context,
                   error: Exception(errMsg),
                 );

+ 21 - 17
lib/ui/payment/stripe_subscription_page.dart

@@ -303,20 +303,22 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
         await _launchStripePortal();
         break;
       case playStore:
-        launchUrlString(
-          "https://play.google.com/store/account/subscriptions?sku=" +
-              _currentSubscription!.productID +
-              "&package=io.ente.photos",
+        unawaited(
+          launchUrlString(
+            "https://play.google.com/store/account/subscriptions?sku=" +
+                _currentSubscription!.productID +
+                "&package=io.ente.photos",
+          ),
         );
         break;
       case appStore:
-        launchUrlString("https://apps.apple.com/account/billing");
+        unawaited(launchUrlString("https://apps.apple.com/account/billing"));
         break;
       default:
         final String capitalizedWord = paymentProvider.isNotEmpty
             ? '${paymentProvider[0].toUpperCase()}${paymentProvider.substring(1).toLowerCase()}'
             : '';
-        showErrorDialog(
+        await showErrorDialog(
           context,
           S.of(context).sorry,
           S.of(context).contactToManageSubscription(capitalizedWord),
@@ -328,7 +330,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
     await _dialog.show();
     try {
       final String url = await _billingService.getStripeCustomerPortalUrl();
-      Navigator.of(context).push(
+      await Navigator.of(context).push(
         MaterialPageRoute(
           builder: (BuildContext context) {
             return WebPage(S.of(context).paymentDetails, url);
@@ -337,7 +339,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
       ).then((value) => onWebPaymentGoBack);
     } catch (e) {
       await _dialog.hide();
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
     }
     await _dialog.hide();
   }
@@ -382,7 +384,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
           confirmAction = choice!.action == ButtonAction.first;
         }
         if (confirmAction) {
-          toggleStripeSubscription(isRenewCancelled);
+          await toggleStripeSubscription(isRenewCancelled);
         }
       },
     );
@@ -398,11 +400,13 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
           : await _billingService.cancelStripeSubscription();
       await _fetchSub();
     } catch (e) {
-      showShortToast(
-        context,
-        isAutoRenewDisabled
-            ? S.of(context).failedToRenew
-            : S.of(context).failedToCancel,
+      unawaited(
+        showShortToast(
+          context,
+          isAutoRenewDisabled
+              ? S.of(context).failedToRenew
+              : S.of(context).failedToCancel,
+        ),
       );
     }
     await _dialog.hide();
@@ -454,7 +458,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
               if (!_isStripeSubscriber &&
                   _hasActiveSubscription &&
                   _currentSubscription!.productID != freeProductID) {
-                showErrorDialog(
+                await showErrorDialog(
                   context,
                   S.of(context).sorry,
                   S.of(context).cancelOtherSubscription(
@@ -473,7 +477,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
                   "addOnBonus ${convertBytesToReadableFormat(addOnBonus)},"
                   "overshooting by ${convertBytesToReadableFormat(_userDetails.getFamilyOrPersonalUsage() - (plan.storage + addOnBonus))}",
                 );
-                showErrorDialog(
+                await showErrorDialog(
                   context,
                   S.of(context).sorry,
                   S.of(context).youCannotDowngradeToThisPlan,
@@ -495,7 +499,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
                   return;
                 }
               }
-              Navigator.push(
+              await Navigator.push(
                 context,
                 MaterialPageRoute(
                   builder: (BuildContext context) {

+ 1 - 1
lib/ui/payment/view_add_on_widget.dart

@@ -33,7 +33,7 @@ class ViewAddOnButton extends StatelessWidget {
         singleBorderRadius: 4,
         alignCaptionedTextToLeft: true,
         onTap: () async {
-          routeToPage(context, AddOnPage(bonusData!));
+          await routeToPage(context, AddOnPage(bonusData!));
         },
       ),
     );

+ 6 - 2
lib/ui/settings/advanced_settings_screen.dart

@@ -1,3 +1,5 @@
+import "dart:async";
+
 import 'package:flutter/material.dart';
 import "package:photos/core/error-reporting/super_logging.dart";
 import "package:photos/generated/l10n.dart";
@@ -150,8 +152,10 @@ class _AdvancedSettingsScreenState extends State<AdvancedSettingsScreen> {
                                 value: () =>
                                     MemoriesService.instance.showMemories,
                                 onChanged: () async {
-                                  MemoriesService.instance.setShowMemories(
-                                    !MemoriesService.instance.showMemories,
+                                  unawaited(
+                                    MemoriesService.instance.setShowMemories(
+                                      !MemoriesService.instance.showMemories,
+                                    ),
                                   );
                                 },
                               ),

+ 1 - 1
lib/ui/settings/backup/backup_folder_selection_page.dart

@@ -232,7 +232,7 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
     } catch (e, s) {
       _logger.severe("Failed to updated backup folder", e, s);
       await dialog.hide();
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
     }
   }
 

+ 4 - 4
lib/ui/settings/backup/backup_section_widget.dart

@@ -94,7 +94,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
             try {
               status = await SyncService.instance.getBackupStatus();
             } catch (e) {
-              showGenericErrorDialog(context: context, error: e);
+              await showGenericErrorDialog(context: context, error: e);
               return;
             }
 
@@ -128,7 +128,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
               duplicates =
                   await DeduplicationService.instance.getDuplicateFiles();
             } catch (e) {
-              showGenericErrorDialog(context: context, error: e);
+              await showGenericErrorDialog(context: context, error: e);
               return;
             }
 
@@ -217,7 +217,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
           ),
       firstButtonLabel: S.of(context).rateUs,
       firstButtonOnTap: () async {
-        UpdateService.instance.launchReviewUrl();
+        await UpdateService.instance.launchReviewUrl();
       },
       firstButtonType: ButtonType.primary,
       secondButtonLabel: S.of(context).ok,
@@ -225,7 +225,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
         showShortToast(
           context,
           S.of(context).remindToEmptyEnteTrash,
-        );
+        ).ignore();
       },
     );
   }

+ 3 - 3
lib/ui/settings/debug_section_widget.dart

@@ -50,7 +50,7 @@ class DebugSectionWidget extends StatelessWidget {
           trailingIconIsMuted: true,
           onTap: () async {
             await LocalSyncService.instance.resetLocalSync();
-            showShortToast(context, "Done");
+            showShortToast(context, "Done").ignore();
           },
         ),
         sectionOptionSpacing,
@@ -63,8 +63,8 @@ class DebugSectionWidget extends StatelessWidget {
           trailingIconIsMuted: true,
           onTap: () async {
             await IgnoredFilesService.instance.reset();
-            SyncService.instance.sync();
-            showShortToast(context, "Done");
+            SyncService.instance.sync().ignore();
+            showShortToast(context, "Done").ignore();
           },
         ),
         sectionOptionSpacing,

+ 1 - 1
lib/ui/settings/general_section_widget.dart

@@ -68,7 +68,7 @@ class GeneralSectionWidget extends StatelessWidget {
           trailingIconIsMuted: true,
           onTap: () async {
             final locale = await getLocale();
-            routeToPage(
+            await routeToPage(
               context,
               LanguageSelectorPage(
                 appSupportedLocales,

+ 1 - 1
lib/ui/settings/security_section_widget.dart

@@ -279,7 +279,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
       }
       await UserService.instance.updateEmailMFA(isEnabled);
     } catch (e) {
-      showToast(context, S.of(context).somethingWentWrong);
+      showToast(context, S.of(context).somethingWentWrong).ignore();
     }
   }
 }

+ 4 - 1
lib/ui/sharing/manage_album_participant.dart

@@ -132,7 +132,10 @@ class _ManageIndividualParticipantState
                               CollectionParticipantRole.viewer,
                             );
                           } catch (e) {
-                            showGenericErrorDialog(context: context, error: e);
+                            await showGenericErrorDialog(
+                              context: context,
+                              error: e,
+                            );
                           }
                           if (isConvertToViewSuccess && mounted) {
                             // reset value

+ 1 - 1
lib/ui/sharing/pickers/device_limit_picker_page.dart

@@ -137,7 +137,7 @@ class _ItemsWidgetState extends State<ItemsWidget> {
     try {
       await CollectionsService.instance.updateShareUrl(widget.collection, prop);
     } catch (e) {
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
       rethrow;
     }
   }

+ 1 - 1
lib/ui/sharing/pickers/link_expiry_picker_page.dart

@@ -180,7 +180,7 @@ class _ItemsWidgetState extends State<ItemsWidget> {
     try {
       await CollectionsService.instance.updateShareUrl(widget.collection, prop);
     } catch (e) {
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
       rethrow;
     }
   }

+ 62 - 47
lib/ui/viewer/actions/file_selection_actions_widget.dart

@@ -1,3 +1,5 @@
+import "dart:async";
+
 import 'package:fast_base58/fast_base58.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -132,18 +134,6 @@ class _FileSelectionActionsWidgetState
       }
     }
 
-    items.add(
-      SelectionActionButton(
-        labelText: S.of(context).share,
-        icon: Icons.adaptive.share_outlined,
-        onTap: () => shareSelected(
-          context,
-          shareButtonKey,
-          widget.selectedFiles.files.toList(),
-        ),
-      ),
-    );
-
     final showUploadIcon = widget.type == GalleryType.localFolder &&
         split.ownedByCurrentUser.isEmpty;
     if (widget.type.showAddToAlbum()) {
@@ -219,17 +209,6 @@ class _FileSelectionActionsWidgetState
       );
     }
 
-    if (widget.type.showDeleteOption()) {
-      items.add(
-        SelectionActionButton(
-          icon: Icons.delete_outline,
-          labelText: S.of(context).delete,
-          onTap: anyOwnedFiles ? _onDeleteClick : null,
-          shouldShow: ownedAndPendingUploadFilesCount > 0,
-        ),
-      );
-    }
-
     if (widget.type.showFavoriteOption()) {
       items.add(
         SelectionActionButton(
@@ -250,6 +229,26 @@ class _FileSelectionActionsWidgetState
       );
     }
 
+    items.add(
+      SelectionActionButton(
+        icon: Icons.grid_view_outlined,
+        labelText: S.of(context).createCollage,
+        onTap: _onCreateCollageClicked,
+        shouldShow: showCollageOption,
+      ),
+    );
+
+    if (widget.type.showDeleteOption()) {
+      items.add(
+        SelectionActionButton(
+          icon: Icons.delete_outline,
+          labelText: S.of(context).delete,
+          onTap: anyOwnedFiles ? _onDeleteClick : null,
+          shouldShow: ownedAndPendingUploadFilesCount > 0,
+        ),
+      );
+    }
+
     if (widget.type.showHideOption()) {
       items.add(
         SelectionActionButton(
@@ -311,29 +310,43 @@ class _FileSelectionActionsWidgetState
 
     items.add(
       SelectionActionButton(
-        icon: Icons.grid_view_outlined,
-        labelText: S.of(context).createCollage,
-        onTap: _onCreateCollageClicked,
-        shouldShow: showCollageOption,
+        labelText: S.of(context).share,
+        icon: Icons.adaptive.share_outlined,
+        onTap: () => shareSelected(
+          context,
+          shareButtonKey,
+          widget.selectedFiles.files.toList(),
+        ),
       ),
     );
 
     if (items.isNotEmpty) {
-      return SizedBox(
-        width: double.infinity,
-        child: Center(
-          child: SingleChildScrollView(
-            physics: const BouncingScrollPhysics(
-              decelerationRate: ScrollDecelerationRate.fast,
-            ),
-            scrollDirection: Axis.horizontal,
-            child: Row(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              children: [
-                const SizedBox(width: 4),
-                ...items,
-                const SizedBox(width: 4),
-              ],
+      final scrollController = ScrollController();
+      // h4ck: https://github.com/flutter/flutter/issues/57920#issuecomment-893970066
+      return MediaQuery(
+        data: MediaQuery.of(context).removePadding(removeBottom: true),
+        child: SafeArea(
+          child: Scrollbar(
+            radius: const Radius.circular(1),
+            thickness: 2,
+            controller: scrollController,
+            thumbVisibility: true,
+            child: SingleChildScrollView(
+              physics: const BouncingScrollPhysics(
+                decelerationRate: ScrollDecelerationRate.fast,
+              ),
+              scrollDirection: Axis.horizontal,
+              child: Container(
+                padding: const EdgeInsets.only(bottom: 24),
+                child: Row(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    const SizedBox(width: 4),
+                    ...items,
+                    const SizedBox(width: 4),
+                  ],
+                ),
+              ),
             ),
           ),
         ),
@@ -489,9 +502,11 @@ class _FileSelectionActionsWidgetState
 
   Future<void> _onCreatedSharedLinkClicked() async {
     if (split.ownedByCurrentUser.isEmpty) {
-      showShortToast(
-        context,
-        S.of(context).canOnlyCreateLinkForFilesOwnedByYou,
+      unawaited(
+        showShortToast(
+          context,
+          S.of(context).canOnlyCreateLinkForFilesOwnedByYou,
+        ),
       );
       return;
     }
@@ -534,7 +549,7 @@ class _FileSelectionActionsWidgetState
         await _copyLink();
       }
       if (actionResult.action == ButtonAction.second) {
-        routeToPage(
+        await routeToPage(
           context,
           ManageSharedLinkWidget(collection: _cachedCollectionForSharedLink),
         );
@@ -555,7 +570,7 @@ class _FileSelectionActionsWidgetState
       final String url =
           "${_cachedCollectionForSharedLink!.publicURLs?.first?.url}#$collectionKey";
       await Clipboard.setData(ClipboardData(text: url));
-      showShortToast(context, S.of(context).linkCopiedToClipboard);
+      unawaited(showShortToast(context, S.of(context).linkCopiedToClipboard));
     }
   }
 

+ 6 - 6
lib/ui/viewer/file/file_app_bar.dart

@@ -248,15 +248,15 @@ class FileAppBarState extends State<FileAppBar> {
         },
         onSelected: (dynamic value) async {
           if (value == 1) {
-            _download(widget.file);
+            await _download(widget.file);
           } else if (value == 2) {
             await _toggleFileArchiveStatus(widget.file);
           } else if (value == 3) {
-            _setAs(widget.file);
+            await _setAs(widget.file);
           } else if (value == 4) {
-            _handleHideRequest(context);
+            await _handleHideRequest(context);
           } else if (value == 5) {
-            _handleUnHideRequest(context);
+            await _handleUnHideRequest(context);
           }
         },
       ),
@@ -362,7 +362,7 @@ class FileAppBarState extends State<FileAppBar> {
     } catch (e) {
       _logger.warning("Failed to save file", e);
       await dialog.hide();
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
     } finally {
       PhotoManager.startChangeNotify();
       LocalSyncService.instance.checkAndSync().ignore();
@@ -423,7 +423,7 @@ class FileAppBarState extends State<FileAppBar> {
     } catch (e) {
       dialog.hide();
       _logger.severe("Failed to use as", e);
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
     }
   }
 }

+ 5 - 1
lib/ui/viewer/file/video_widget_new.dart

@@ -90,10 +90,11 @@ class _VideoWidgetNewState extends State<VideoWidgetNew>
 
   @override
   void dispose() {
+    removeCallBack(widget.file);
+    _progressNotifier.dispose();
     WidgetsBinding.instance.removeObserver(this);
     playingStreamSubscription.cancel();
     player.dispose();
-    _progressNotifier.dispose();
     super.dispose();
   }
 
@@ -159,6 +160,9 @@ class _VideoWidgetNewState extends State<VideoWidgetNew>
     getFileFromServer(
       widget.file,
       progressCallback: (count, total) {
+        if(!mounted) {
+          return;
+        }
         _progressNotifier.value = count / (widget.file.fileSize ?? total);
         if (_progressNotifier.value == 1) {
           if (mounted) {

+ 17 - 17
lib/ui/viewer/file/zoomable_image.dart

@@ -105,21 +105,19 @@ class _ZoomableImageState extends State<ZoomableImage>
     } else {
       content = const EnteLoadingWidget();
     }
-    final GestureDragUpdateCallback? verticalDragCallback = _isZooming
-        ? null
-        : (d) => {
-              if (!_isZooming)
+    verticalDragCallback(d) => {
+          if (!_isZooming)
+            {
+              if (d.delta.dy > dragSensitivity)
                 {
-                  if (d.delta.dy > dragSensitivity)
-                    {
-                      {Navigator.of(context).pop()},
-                    }
-                  else if (d.delta.dy < (dragSensitivity * -1))
-                    {
-                      showDetailsSheet(context, widget.photo),
-                    },
+                  {Navigator.of(context).pop()},
+                }
+              else if (d.delta.dy < (dragSensitivity * -1))
+                {
+                  showDetailsSheet(context, widget.photo),
                 },
-            };
+            },
+        };
 
     return GestureDetector(
       onVerticalDragUpdate: verticalDragCallback,
@@ -268,7 +266,7 @@ class _ZoomableImageState extends State<ZoomableImage>
         showToast(
           context,
           'Updating photo scale zooming and scale: ${_photoViewController.scale}',
-        );
+        ).ignore();
       }
       final prevImageInfo = await getImageInfo(previewImageProvider);
       finalImageInfo = await getImageInfo(finalImageProvider);
@@ -303,10 +301,12 @@ class _ZoomableImageState extends State<ZoomableImage>
   ) async {
     final int h = imageInfo.image.height, w = imageInfo.image.width;
     if (h != enteFile.height || w != enteFile.width) {
-      if (kDebugMode) {
-        showToast(context, 'Updating aspect ratio');
+      final logMessage =
+          'Updating aspect ratio for from ${enteFile.height}x${enteFile.width} to ${h}x$w';
+      if (kDebugMode && (enteFile.height != 0 || enteFile.width != 0)) {
+        showToast(context, logMessage).ignore();
       }
-      _logger.info('Updating aspect ratio for $enteFile to $h:$w');
+      _logger.info(logMessage);
       await FileMagicService.instance.updatePublicMagicMetadata([
         enteFile,
       ], {

+ 4 - 0
lib/ui/viewer/file/zoomable_live_image_new.dart

@@ -10,6 +10,7 @@ import "package:photos/models/file/extensions/file_props.dart";
 import 'package:photos/models/file/file.dart';
 import "package:photos/models/metadata/file_magic.dart";
 import "package:photos/services/file_magic_service.dart";
+import "package:photos/services/local_file_update_service.dart";
 import 'package:photos/ui/viewer/file/zoomable_image.dart';
 import 'package:photos/utils/file_util.dart';
 import 'package:photos/utils/toast_util.dart';
@@ -46,6 +47,9 @@ class _ZoomableLiveImageNewState extends State<ZoomableLiveImageNew>
     _logger.info(
       'initState for ${_enteFile.generatedID} with tag ${_enteFile.tag} and name ${_enteFile.displayName}',
     );
+    if (_enteFile.isLivePhoto && _enteFile.isUploaded) {
+      LocalFileUpdateService.instance.checkLivePhoto(_enteFile).ignore();
+    }
     super.initState();
   }
 

+ 7 - 3
lib/ui/viewer/file_details/favorite_widget.dart

@@ -1,3 +1,5 @@
+import "dart:async";
+
 import "package:flutter/material.dart";
 import "package:like_button/like_button.dart";
 import "package:logging/logging.dart";
@@ -77,9 +79,11 @@ class _FavoriteWidgetState extends State<FavoriteWidget> {
               } catch (e, s) {
                 _logger.severe(e, s);
                 hasError = true;
-                showToast(
-                  context,
-                  S.of(context).sorryCouldNotRemoveFromFavorites,
+                unawaited(
+                  showToast(
+                    context,
+                    S.of(context).sorryCouldNotRemoveFromFavorites,
+                  ),
                 );
               }
             }

+ 1 - 1
lib/ui/viewer/gallery/empty_album_state.dart

@@ -31,7 +31,7 @@ class EmptyAlbumState extends StatelessWidget {
               try {
                 await showAddPhotosSheet(context, c);
               } catch (e) {
-                showGenericErrorDialog(context: context, error: e);
+                await showGenericErrorDialog(context: context, error: e);
               }
             },
           ),

+ 2 - 0
lib/ui/viewer/gallery/gallery.dart

@@ -131,6 +131,8 @@ class GalleryState extends State<Gallery> {
               "Reloaded gallery on soft refresh all files on ${event.reason}",
             );
           }
+
+          setState(() {});
         });
       });
     }

+ 58 - 25
lib/ui/viewer/gallery/gallery_app_bar_widget.dart

@@ -71,6 +71,7 @@ enum AlbumPopupAction {
   addPhotos,
   pinAlbum,
   removeLink,
+  cleanUncategorized,
 }
 
 class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
@@ -130,11 +131,14 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
     if (galleryType != GalleryType.ownedCollection &&
         galleryType != GalleryType.hiddenOwnedCollection &&
         galleryType != GalleryType.quickLink) {
-      showToast(
-        context,
-        'Type of galler $galleryType is not supported for '
-        'rename',
+      unawaited(
+        showToast(
+          context,
+          'Type of galler $galleryType is not supported for '
+          'rename',
+        ),
       );
+
       return;
     }
     final result = await showTextInputDialog(
@@ -172,7 +176,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
       },
     );
     if (result is Exception) {
-      showGenericErrorDialog(context: context, error: result);
+      await showGenericErrorDialog(context: context, error: result);
     }
   }
 
@@ -204,7 +208,10 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
     );
     if (actionResult?.action != null && mounted) {
       if (actionResult!.action == ButtonAction.error) {
-        showGenericErrorDialog(context: context, error: actionResult.exception);
+        await showGenericErrorDialog(
+          context: context,
+          error: actionResult.exception,
+        );
       } else if (actionResult.action == ButtonAction.first) {
         Navigator.of(context).pop();
       }
@@ -224,13 +231,13 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
           .getBackupStatus(pathID: widget.deviceCollection!.id);
     } catch (e) {
       await dialog.hide();
-      showGenericErrorDialog(context: context, error: e);
+      unawaited(showGenericErrorDialog(context: context, error: e));
       return;
     }
 
     await dialog.hide();
     if (status.localIDs.isEmpty) {
-      showErrorDialog(
+      await showErrorDialog(
         context,
         S.of(context).allClear,
         S.of(context).youveNoFilesInThisAlbumThatCanBeDeleted,
@@ -253,7 +260,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
       body: S.of(context).youHaveSuccessfullyFreedUp(formatBytes(status.size)),
       firstButtonLabel: S.of(context).rateUs,
       firstButtonOnTap: () async {
-        UpdateService.instance.launchReviewUrl();
+        await UpdateService.instance.launchReviewUrl();
       },
       firstButtonType: ButtonType.primary,
       secondButtonLabel: S.of(context).ok,
@@ -262,7 +269,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
           showToast(
             context,
             S.of(context).remindToEmptyDeviceTrash,
-          );
+          ).ignore();
         }
       },
     );
@@ -379,6 +386,25 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
         ),
       );
     }
+
+    if (galleryType == GalleryType.uncategorized) {
+      items.add(
+        PopupMenuItem(
+          value: AlbumPopupAction.cleanUncategorized,
+          child: Row(
+            children: [
+              const Icon(Icons.crop_original_outlined),
+              const Padding(
+                padding: EdgeInsets.all(8),
+              ),
+              Text(
+                "Clean Uncategorized",
+              ),
+            ],
+          ),
+        ),
+      );
+    }
     if (galleryType.canPin()) {
       items.add(
         PopupMenuItem(
@@ -582,8 +608,13 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
               }
             } else if (value == AlbumPopupAction.map) {
               await showOnMap();
+            } else if (value == AlbumPopupAction.cleanUncategorized) {
+              await collectionActions.removeFromUncatIfPresentInOtherAlbum(
+                widget.collection!,
+                context,
+              );
             } else {
-              showToast(context, S.of(context).somethingWentWrong);
+              unawaited(showToast(context, S.of(context).somethingWentWrong));
             }
           },
         ),
@@ -599,21 +630,23 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
       widget.collection!,
     );
     if (coverPhotoID != null) {
-      changeCoverPhoto(context, widget.collection!, coverPhotoID);
+      unawaited(changeCoverPhoto(context, widget.collection!, coverPhotoID));
     }
   }
 
   Future<void> showOnMap() async {
     final bool result = await requestForMapEnable(context);
     if (result) {
-      Navigator.of(context).push(
-        MaterialPageRoute(
-          builder: (context) => MapScreen(
-            filesFutureFn: () async {
-              return FilesDB.instance.getAllFilesCollection(
-                widget.collection!.id,
-              );
-            },
+      unawaited(
+        Navigator.of(context).push(
+          MaterialPageRoute(
+            builder: (context) => MapScreen(
+              filesFutureFn: () async {
+                return FilesDB.instance.getAllFilesCollection(
+                  widget.collection!.id,
+                );
+              },
+            ),
           ),
         ),
       );
@@ -641,7 +674,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
       ],
     );
     if (sortByAsc != null) {
-      changeSortOrder(bContext, widget.collection!, sortByAsc);
+      unawaited(changeSortOrder(bContext, widget.collection!, sortByAsc));
     }
   }
 
@@ -664,7 +697,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
       } catch (e, s) {
         _logger.severe("failed to trash collection", e, s);
         await dialog.hide();
-        showGenericErrorDialog(context: context, error: e);
+        await showGenericErrorDialog(context: context, error: e);
       }
     } else {
       final bool result = await collectionActions.deleteCollectionSheet(
@@ -691,7 +724,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
       }
     } catch (e, s) {
       _logger.severe("failed to trash collection", e, s);
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
     }
   }
 
@@ -726,7 +759,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
       }
     } catch (e, s) {
       _logger.severe(e, s);
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
     }
   }
 
@@ -736,7 +769,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
       await showAddPhotosSheet(bContext, collection!);
     } catch (e, s) {
       _logger.severe(e, s);
-      showGenericErrorDialog(context: bContext, error: e);
+      await showGenericErrorDialog(context: bContext, error: e);
     }
   }
 

+ 1 - 1
lib/ui/viewer/gallery/hooks/add_photos_sheet.dart

@@ -205,7 +205,7 @@ class AddPhotosPhotoWidget extends StatelessWidget {
       if (e is StateError) {
         final PermissionState ps = await PhotoManager.requestPermissionExtend();
         if (ps != PermissionState.authorized && ps != PermissionState.limited) {
-          showChoiceDialog(
+          await showChoiceDialog(
             context,
             title: context.l10n.grantPermission,
             body: context.l10n.pleaseGrantPermissions,

+ 36 - 11
lib/ui/viewer/location/location_screen.dart

@@ -1,3 +1,4 @@
+import "dart:async";
 import 'dart:developer' as dev;
 
 import "package:flutter/material.dart";
@@ -118,7 +119,7 @@ class LocationScreenPopUpMenu extends StatelessWidget {
                 );
                 Navigator.of(context).pop();
               } catch (e) {
-                showGenericErrorDialog(context: context, error: e);
+                await showGenericErrorDialog(context: context, error: e);
               }
             }
           },
@@ -138,15 +139,17 @@ class LocationGalleryWidget extends StatefulWidget {
 
 class _LocationGalleryWidgetState extends State<LocationGalleryWidget> {
   late final Future<FileLoadResult> fileLoadResult;
+  late final List<EnteFile> allFilesWithLocation;
 
   late Widget galleryHeaderWidget;
   final _selectedFiles = SelectedFiles();
+  late final StreamSubscription<LocalPhotosUpdatedEvent> _filesUpdateEvent;
   @override
   void initState() {
     final collectionsToHide =
         CollectionsService.instance.archivedOrHiddenCollectionIds();
-    fileLoadResult =
-        FilesDB.instance.fetchAllUploadedAndSharedFilesWithLocation(
+    fileLoadResult = FilesDB.instance
+        .fetchAllUploadedAndSharedFilesWithLocation(
       galleryLoadStartTime,
       galleryLoadEndTime,
       limit: null,
@@ -155,14 +158,35 @@ class _LocationGalleryWidgetState extends State<LocationGalleryWidget> {
         ignoredCollectionIDs: collectionsToHide,
         hideIgnoredForUpload: true,
       ),
-    );
+    )
+        .then((value) {
+      allFilesWithLocation = value.files;
+      _filesUpdateEvent =
+          Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
+        if (event.type == EventType.deletedFromDevice ||
+            event.type == EventType.deletedFromEverywhere ||
+            event.type == EventType.deletedFromRemote ||
+            event.type == EventType.hide) {
+          for (var updatedFile in event.updatedFiles) {
+            allFilesWithLocation.remove(updatedFile);
+          }
+          if (mounted) {
+            setState(() {});
+          }
+        }
+      });
+      return value;
+    });
+
     galleryHeaderWidget = const GalleryHeaderWidget();
+
     super.initState();
   }
 
   @override
   void dispose() {
     InheritedLocationScreenState.memoryCountNotifier.value = null;
+    _filesUpdateEvent.cancel();
     super.dispose();
   }
 
@@ -174,12 +198,13 @@ class _LocationGalleryWidgetState extends State<LocationGalleryWidget> {
         .locationTagEntity
         .item
         .centerPoint;
+
     Future<FileLoadResult> filterFiles() async {
-      final FileLoadResult result = await fileLoadResult;
-      //wait for ignored files to be removed after init
+      //waiting for allFilesWithLocation to be initialized
+      await fileLoadResult;
       final stopWatch = Stopwatch()..start();
-      final copyOfFiles = List<EnteFile>.from(result.files);
-      copyOfFiles.removeWhere((f) {
+      final filesInLocation = allFilesWithLocation;
+      filesInLocation.removeWhere((f) {
         return !LocationService.instance.isFileInsideLocationTag(
           centerPoint,
           f.location!,
@@ -191,12 +216,12 @@ class _LocationGalleryWidgetState extends State<LocationGalleryWidget> {
       );
       stopWatch.stop();
       InheritedLocationScreenState.memoryCountNotifier.value =
-          copyOfFiles.length;
+          filesInLocation.length;
 
       return Future.value(
         FileLoadResult(
-          copyOfFiles,
-          result.hasMore,
+          filesInLocation,
+          false,
         ),
       );
     }

+ 44 - 10
lib/ui/viewer/search/result/search_result_page.dart

@@ -1,3 +1,5 @@
+import "dart:async";
+
 import 'package:flutter/material.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/events/files_updated_event.dart';
@@ -11,25 +13,56 @@ import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart';
 import 'package:photos/ui/viewer/gallery/gallery.dart';
 import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart';
 
-class SearchResultPage extends StatelessWidget {
+class SearchResultPage extends StatefulWidget {
   final SearchResult searchResult;
   final bool enableGrouping;
   final String tagPrefix;
 
-  final _selectedFiles = SelectedFiles();
   static const GalleryType appBarType = GalleryType.searchResults;
   static const GalleryType overlayType = GalleryType.searchResults;
 
-  SearchResultPage(
+  const SearchResultPage(
     this.searchResult, {
     this.enableGrouping = true,
     this.tagPrefix = "",
     Key? key,
   }) : super(key: key);
 
+  @override
+  State<SearchResultPage> createState() => _SearchResultPageState();
+}
+
+class _SearchResultPageState extends State<SearchResultPage> {
+  final _selectedFiles = SelectedFiles();
+  late final List<EnteFile> files;
+  late final StreamSubscription<LocalPhotosUpdatedEvent> _filesUpdatedEvent;
+
+  @override
+  void initState() {
+    super.initState();
+    files = widget.searchResult.resultFiles();
+    _filesUpdatedEvent =
+        Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
+      if (event.type == EventType.deletedFromDevice ||
+          event.type == EventType.deletedFromEverywhere ||
+          event.type == EventType.deletedFromRemote ||
+          event.type == EventType.hide) {
+        for (var updatedFile in event.updatedFiles) {
+          files.remove(updatedFile);
+        }
+        setState(() {});
+      }
+    });
+  }
+
+  @override
+  void dispose() {
+    _filesUpdatedEvent.cancel();
+    super.dispose();
+  }
+
   @override
   Widget build(BuildContext context) {
-    final List<EnteFile> files = searchResult.resultFiles();
     final gallery = Gallery(
       asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) {
         final result = files
@@ -50,18 +83,19 @@ class SearchResultPage extends StatelessWidget {
       removalEventTypes: const {
         EventType.deletedFromRemote,
         EventType.deletedFromEverywhere,
+        EventType.hide,
       },
-      tagPrefix: tagPrefix + searchResult.heroTag(),
+      tagPrefix: widget.tagPrefix + widget.searchResult.heroTag(),
       selectedFiles: _selectedFiles,
-      enableFileGrouping: enableGrouping,
-      initialFiles: [searchResult.resultFiles().first],
+      enableFileGrouping: widget.enableGrouping,
+      initialFiles: [widget.searchResult.resultFiles().first],
     );
     return Scaffold(
       appBar: PreferredSize(
         preferredSize: const Size.fromHeight(50.0),
         child: GalleryAppBarWidget(
-          appBarType,
-          searchResult.name(),
+          SearchResultPage.appBarType,
+          widget.searchResult.name(),
           _selectedFiles,
         ),
       ),
@@ -70,7 +104,7 @@ class SearchResultPage extends StatelessWidget {
         children: [
           gallery,
           FileSelectionOverlayBar(
-            overlayType,
+            SearchResultPage.overlayType,
             _selectedFiles,
           ),
         ],

+ 0 - 1
lib/utils/debouncer.dart

@@ -6,7 +6,6 @@ import "package:photos/models/typedefs.dart";
 class Debouncer {
   final Duration _duration;
 
-  ///in milliseconds
   final ValueNotifier<bool> _debounceActiveNotifier = ValueNotifier(false);
 
   /// If executionIntervalInSeconds is not null, then the debouncer will execute the

+ 8 - 5
lib/utils/delete_file_util.dart

@@ -102,7 +102,7 @@ Future<void> deleteFilesFromEverywhere(
       await FilesDB.instance.deleteMultipleUploadedFiles(fileIDs);
     } catch (e) {
       _logger.severe(e);
-      showGenericErrorDialog(context: context, error: e);
+      await showGenericErrorDialog(context: context, error: e);
       rethrow;
     }
     for (final collectionID in updatedCollectionIDs) {
@@ -127,9 +127,9 @@ Future<void> deleteFilesFromEverywhere(
       ),
     );
     if (hasLocalOnlyFiles && Platform.isAndroid) {
-      showShortToast(context, S.of(context).filesDeleted);
+      await showShortToast(context, S.of(context).filesDeleted);
     } else {
-      showShortToast(context, S.of(context).movedToTrash);
+      await showShortToast(context, S.of(context).movedToTrash);
     }
   }
   if (uploadedFilesToBeTrashed.isNotEmpty) {
@@ -163,7 +163,7 @@ Future<void> deleteFilesFromRemoteOnly(
     await FilesDB.instance.deleteMultipleUploadedFiles(uploadedFileIDs);
   } catch (e, s) {
     _logger.severe("Failed to delete files from remote", e, s);
-    showGenericErrorDialog(context: context, error: e);
+    await showGenericErrorDialog(context: context, error: e);
     rethrow;
   }
   for (final collectionID in updatedCollectionIDs) {
@@ -626,7 +626,10 @@ Future<void> showDeleteSheet(
   );
   if (actionResult?.action != null &&
       actionResult!.action == ButtonAction.error) {
-    showGenericErrorDialog(context: context, error: actionResult.exception);
+    await showGenericErrorDialog(
+      context: context,
+      error: actionResult.exception,
+    );
   } else {
     selectedFiles.clearAll();
   }

+ 54 - 32
lib/utils/file_uploader.dart

@@ -43,6 +43,7 @@ class FileUploader {
   static const kMaximumConcurrentVideoUploads = 2;
   static const kMaximumThumbnailCompressionAttempts = 2;
   static const kMaximumUploadAttempts = 4;
+  static const kMaxFileSize5Gib = 5368709120;
   static const kBlockedUploadsPollFrequency = Duration(seconds: 2);
   static const kFileUploadTimeout = Duration(minutes: 50);
   static const k20MBStorageBuffer = 20 * 1024 * 1024;
@@ -68,6 +69,11 @@ class FileUploader {
   late ProcessType _processType;
   late bool _isBackground;
   late SharedPreferences _prefs;
+  // _hasInitiatedForceUpload is used to track if user attempted force upload
+  // where files are uploaded directly (without adding them to DB). In such
+  // cases, we don't want to clear the stale upload files. See #removeStaleFiles
+  // as it can result in clearing files which are still being force uploaded.
+  bool _hasInitiatedForceUpload = false;
 
   FileUploader._privateConstructor() {
     Bus.instance.on<SubscriptionPurchasedEvent>().listen((event) {
@@ -285,6 +291,12 @@ class FileUploader {
   }
 
   Future<void> removeStaleFiles() async {
+    if (_hasInitiatedForceUpload) {
+      _logger.info(
+        "Force upload was initiated, skipping stale file cleanup",
+      );
+      return;
+    }
     try {
       final String dir = Configuration.instance.getTempDirectory();
       // delete all files in the temp directory that start with upload_ and
@@ -324,6 +336,7 @@ class FileUploader {
   }
 
   Future<EnteFile> forceUpload(EnteFile file, int collectionID) async {
+    _hasInitiatedForceUpload = true;
     return _tryToUpload(file, collectionID, true);
   }
 
@@ -381,15 +394,9 @@ class FileUploader {
         'starting ${forcedUpload ? 'forced' : ''} '
         '${isUpdatedFile ? 're-upload' : 'upload'} of ${file.toString()}',
       );
-      try {
-        mediaUploadData = await getUploadDataFromEnteFile(file);
-      } catch (e) {
-        if (e is InvalidFileError) {
-          await _onInvalidFileError(file, e);
-        } else {
-          rethrow;
-        }
-      }
+
+      mediaUploadData = await getUploadDataFromEnteFile(file);
+
       Uint8List? key;
       if (isUpdatedFile) {
         key = getFileKey(file);
@@ -399,7 +406,7 @@ class FileUploader {
         // uploaded file. If map is found, it also returns the corresponding
         // mapped or update file entry.
         final result = await _mapToExistingUploadWithSameHash(
-          mediaUploadData!,
+          mediaUploadData,
           file,
           collectionID,
         );
@@ -416,7 +423,7 @@ class FileUploader {
       if (File(encryptedFilePath).existsSync()) {
         await File(encryptedFilePath).delete();
       }
-      await _checkIfWithinStorageLimit(mediaUploadData!.sourceFile!);
+      await _checkIfWithinStorageLimit(mediaUploadData.sourceFile!);
       final encryptedFile = File(encryptedFilePath);
       final EncryptionResult fileAttributes = await CryptoUtil.encryptFile(
         mediaUploadData.sourceFile!.path,
@@ -541,9 +548,14 @@ class FileUploader {
           e is StorageLimitExceededError ||
           e is WiFiUnavailableError ||
           e is SilentlyCancelUploadsError ||
+          e is InvalidFileError ||
           e is FileTooLargeForPlanError)) {
         _logger.severe("File upload failed for " + file.toString(), e, s);
       }
+      if (e is InvalidFileError) {
+        _logger.severe("File upload ignored for " + file.toString(), e);
+        await _onInvalidFileError(file, e);
+      }
       if ((e is StorageLimitExceededError ||
           e is FileTooLargeForPlanError ||
           e is NoActiveSubscriptionError)) {
@@ -749,8 +761,15 @@ class FileUploader {
             'freeStorage $freeStorage');
         throw StorageLimitExceededError();
       }
+      if (fileSize > kMaxFileSize5Gib) {
+        _logger.warning('File size exceeds 5GiB fileSize $fileSize');
+        throw InvalidFileError(
+          'file size above 5GiB',
+          InvalidReason.tooLargeFile,
+        );
+      }
     } catch (e) {
-      if (e is StorageLimitExceededError) {
+      if (e is StorageLimitExceededError || e is InvalidFileError) {
         rethrow;
       } else {
         _logger.severe('Error checking storage limit', e);
@@ -759,28 +778,31 @@ class FileUploader {
   }
 
   Future _onInvalidFileError(EnteFile file, InvalidFileError e) async {
-    final bool canIgnoreFile = file.localID != null &&
-        file.deviceFolder != null &&
-        file.title != null &&
-        !file.isSharedMediaToAppSandbox;
-    // If the file is not uploaded yet and either it can not be ignored or the
-    // err is related to live photo media, delete the local entry
-    final bool deleteEntry =
-        !file.isUploaded && (!canIgnoreFile || e.reason.isLivePhotoErr);
+    try {
+      final bool canIgnoreFile = file.localID != null &&
+          file.deviceFolder != null &&
+          file.title != null &&
+          !file.isSharedMediaToAppSandbox;
+      // If the file is not uploaded yet and either it can not be ignored or the
+      // err is related to live photo media, delete the local entry
+      final bool deleteEntry =
+          !file.isUploaded && (!canIgnoreFile || e.reason.isLivePhotoErr);
 
-    if (e.reason != InvalidReason.thumbnailMissing || !canIgnoreFile) {
-      _logger.severe(
-        "Invalid file, localDelete: $deleteEntry, ignored: $canIgnoreFile",
-        e,
-      );
-    }
-    if (deleteEntry) {
-      await FilesDB.instance.deleteLocalFile(file);
-    }
-    if (canIgnoreFile) {
-      await LocalSyncService.instance.ignoreUpload(file, e);
+      if (e.reason != InvalidReason.thumbnailMissing || !canIgnoreFile) {
+        _logger.severe(
+          "Invalid file, localDelete: $deleteEntry, ignored: $canIgnoreFile",
+          e,
+        );
+      }
+      if (deleteEntry) {
+        await FilesDB.instance.deleteLocalFile(file);
+      }
+      if (canIgnoreFile) {
+        await LocalSyncService.instance.ignoreUpload(file, e);
+      }
+    } catch (e, s) {
+      _logger.severe("Failed to handle invalid file error", e, s);
     }
-    throw e;
   }
 
   Future<EnteFile> _uploadFile(

+ 37 - 7
lib/utils/file_uploader_util.dart

@@ -5,6 +5,7 @@ import 'dart:typed_data';
 import 'dart:ui' as ui;
 
 import "package:archive/archive_io.dart";
+import "package:computer/computer.dart";
 import 'package:logging/logging.dart';
 import "package:motion_photos/motion_photos.dart";
 import 'package:motionphoto/motionphoto.dart';
@@ -21,6 +22,7 @@ import "package:photos/models/metadata/file_magic.dart";
 import "package:photos/services/file_magic_service.dart";
 import 'package:photos/utils/crypto_util.dart';
 import 'package:photos/utils/file_util.dart';
+import "package:uuid/uuid.dart";
 import 'package:video_thumbnail/video_thumbnail.dart';
 
 final _logger = Logger("FileUtil");
@@ -125,13 +127,14 @@ Future<MediaUploadData> _getMediaUploadDataFromAssetFile(EnteFile file) async {
     fileHash = '$fileHash$kLivePhotoHashSeparator$livePhotoVideoHash';
     final tempPath = Configuration.instance.getTempDirectory();
     // .elp -> ente live photo
-    final livePhotoPath = tempPath + file.generatedID.toString() + ".elp";
-    _logger.fine("Uploading zipped live photo from " + livePhotoPath);
-    final encoder = ZipFileEncoder();
-    encoder.create(livePhotoPath);
-    await encoder.addFile(videoUrl, "video" + extension(videoUrl.path));
-    await encoder.addFile(sourceFile, "image" + extension(sourceFile.path));
-    encoder.close();
+    final uniqueId = const Uuid().v4().toString();
+    final livePhotoPath = tempPath + uniqueId + "_${file.generatedID}.elp";
+    _logger.fine("Creating zip for live photo from " + livePhotoPath);
+    await zip(
+      zipPath: livePhotoPath,
+      imagePath: sourceFile.path,
+      videoPath: videoUrl.path,
+    );
     // delete the temporary video and image copy (only in IOS)
     if (Platform.isIOS) {
       await sourceFile.delete();
@@ -168,6 +171,33 @@ Future<MediaUploadData> _getMediaUploadDataFromAssetFile(EnteFile file) async {
   );
 }
 
+Future<void> _computeZip(Map<String, dynamic> args) async {
+  final String zipPath = args['zipPath'];
+  final String imagePath = args['imagePath'];
+  final String videoPath = args['videoPath'];
+  final encoder = ZipFileEncoder();
+  encoder.create(zipPath);
+  await encoder.addFile(File(imagePath), "image" + extension(imagePath));
+  await encoder.addFile(File(videoPath), "video" + extension(videoPath));
+  encoder.close();
+}
+
+Future<void> zip({
+  required String zipPath,
+  required String imagePath,
+  required String videoPath,
+}) {
+  return Computer.shared().compute(
+    _computeZip,
+    param: {
+      'zipPath': zipPath,
+      'imagePath': imagePath,
+      'videoPath': videoPath,
+    },
+    taskName: 'zip',
+  );
+}
+
 Future<Uint8List?> _getThumbnailForUpload(
   AssetEntity asset,
   EnteFile file,

+ 3 - 3
lib/utils/file_util.dart

@@ -204,7 +204,7 @@ Future<File?> _getLivePhotoFromServer(
     return needLiveVideo ? livePhoto.video : livePhoto.image;
   } catch (e, s) {
     _logger.warning("live photo get failed", e, s);
-    _livePhotoDownloadsTracker.remove(downloadID);
+    await _livePhotoDownloadsTracker.remove(downloadID);
     return null;
   }
 }
@@ -348,9 +348,9 @@ Future<Uint8List> compressThumbnail(Uint8List thumbnail) {
 
 Future<void> clearCache(EnteFile file) async {
   if (file.fileType == FileType.video) {
-    VideoCacheManager.instance.removeFile(file.downloadUrl);
+    await VideoCacheManager.instance.removeFile(file.downloadUrl);
   } else {
-    DefaultCacheManager().removeFile(file.downloadUrl);
+    await DefaultCacheManager().removeFile(file.downloadUrl);
   }
   final cachedThumbnail = File(
     Configuration.instance.getThumbnailCacheDirectory() +

+ 9 - 7
lib/utils/magic_util.dart

@@ -1,3 +1,5 @@
+import "dart:async";
+
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:path/path.dart';
@@ -114,7 +116,7 @@ Future<void> changeSortOrder(
     );
   } catch (e, s) {
     _logger.severe("failed to update collection visibility", e, s);
-    showShortToast(context, S.of(context).somethingWentWrong);
+    unawaited(showShortToast(context, S.of(context).somethingWentWrong));
     rethrow;
   }
 }
@@ -134,7 +136,7 @@ Future<void> updateOrder(
     );
   } catch (e, s) {
     _logger.severe("failed to update order", e, s);
-    showShortToast(context, S.of(context).somethingWentWrong);
+    unawaited(showShortToast(context, S.of(context).somethingWentWrong));
     rethrow;
   }
 }
@@ -160,7 +162,7 @@ Future<void> changeCoverPhoto(
     );
   } catch (e, s) {
     _logger.severe("failed to update cover", e, s);
-    showShortToast(context, S.of(context).somethingWentWrong);
+    unawaited(showShortToast(context, S.of(context).somethingWentWrong));
     rethrow;
   }
 }
@@ -179,7 +181,7 @@ Future<bool> editTime(
     );
     return true;
   } catch (e) {
-    showShortToast(context, S.of(context).somethingWentWrong);
+    showShortToast(context, S.of(context).somethingWentWrong).ignore();
     return false;
   }
 }
@@ -218,7 +220,7 @@ Future<void> editFilename(
   );
   if (result is Exception) {
     _logger.severe("Failed to rename file");
-    showGenericErrorDialog(context: context, error: result);
+    await showGenericErrorDialog(context: context, error: result);
   }
 }
 
@@ -238,7 +240,7 @@ Future<bool> editFileCaption(
     return true;
   } catch (e) {
     if (context != null) {
-      showShortToast(context, S.of(context).somethingWentWrong);
+      unawaited(showShortToast(context, S.of(context).somethingWentWrong));
     }
     return false;
   }
@@ -265,7 +267,7 @@ Future<void> _updatePublicMetadata(
     await FileMagicService.instance.updatePublicMagicMetadata(files, update);
     if (context != null) {
       if (showDoneToast) {
-        showShortToast(context, S.of(context).done);
+        await showShortToast(context, S.of(context).done);
       }
       await dialog?.hide();
     }

+ 6 - 5
pubspec.lock

@@ -518,11 +518,12 @@ packages:
   file_saver:
     dependency: "direct main"
     description:
-      name: file_saver
-      sha256: "591d25e750e3a4b654f7b0293abc2ed857242f82ca7334051b2a8ceeb369dac8"
-      url: "https://pub.dev"
-    source: hosted
-    version: "0.2.8"
+      path: "."
+      ref: HEAD
+      resolved-ref: "01b2e6b6fe520cfa5d2d91342ccbfbaefa8f6482"
+      url: "https://github.com/jesims/file_saver.git"
+    source: git
+    version: "0.2.9"
   firebase_core:
     dependency: "direct main"
     description:

+ 5 - 3
pubspec.yaml

@@ -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.8.9+529
+version: 0.8.13+533
 
 environment:
   sdk: ">=3.0.0 <4.0.0"
@@ -57,8 +57,10 @@ dependencies:
   extended_image: ^8.1.1
   fade_indexed_stack: ^0.2.2
   fast_base58: ^0.2.1
-  # https://github.com/incrediblezayed/file_saver/issues/86
-  file_saver: 0.2.8
+
+  file_saver:
+    # Use forked version till this PR is merged: https://github.com/incrediblezayed/file_saver/pull/87
+    git: https://github.com/jesims/file_saver.git
   firebase_core: ^2.13.1
   firebase_messaging: ^14.6.2
   fk_user_agent: ^2.0.1