浏览代码

Merge branch 'main' into homewidget

Prateek Sunal 1 年之前
父节点
当前提交
95588c8aef
共有 48 个文件被更改,包括 821 次插入174 次删除
  1. 12 0
      CHANGELOG.md
  2. 3 0
      lib/events/pause_video_event.dart
  3. 50 0
      lib/gateways/cast_gw.dart
  4. 1 0
      lib/generated/intl/messages_cs.dart
  5. 2 1
      lib/generated/intl/messages_de.dart
  6. 10 1
      lib/generated/intl/messages_en.dart
  7. 2 1
      lib/generated/intl/messages_es.dart
  8. 2 1
      lib/generated/intl/messages_fr.dart
  9. 2 1
      lib/generated/intl/messages_it.dart
  10. 1 0
      lib/generated/intl/messages_ko.dart
  11. 10 0
      lib/generated/intl/messages_nl.dart
  12. 1 0
      lib/generated/intl/messages_no.dart
  13. 1 0
      lib/generated/intl/messages_pl.dart
  14. 1 0
      lib/generated/intl/messages_pt.dart
  15. 7 0
      lib/generated/intl/messages_zh.dart
  16. 62 2
      lib/generated/l10n.dart
  17. 2 1
      lib/l10n/intl_cs.arb
  18. 3 2
      lib/l10n/intl_de.arb
  19. 9 3
      lib/l10n/intl_en.arb
  20. 3 2
      lib/l10n/intl_es.arb
  21. 3 2
      lib/l10n/intl_fr.arb
  22. 3 2
      lib/l10n/intl_it.arb
  23. 2 1
      lib/l10n/intl_ko.arb
  24. 7 1
      lib/l10n/intl_nl.arb
  25. 2 1
      lib/l10n/intl_no.arb
  26. 2 1
      lib/l10n/intl_pl.arb
  27. 2 1
      lib/l10n/intl_pt.arb
  28. 7 1
      lib/l10n/intl_zh.arb
  29. 17 0
      lib/services/collections_service.dart
  30. 1 4
      lib/services/location_service.dart
  31. 1 1
      lib/services/update_service.dart
  32. 5 0
      lib/ui/components/info_item_widget.dart
  33. 6 0
      lib/ui/map/enable_map.dart
  34. 12 4
      lib/ui/map/map_marker.dart
  35. 68 42
      lib/ui/map/map_screen.dart
  36. 71 46
      lib/ui/map/map_view.dart
  37. 17 16
      lib/ui/map/tile/attribution/map_attribution.dart
  38. 25 4
      lib/ui/map/tile/layers.dart
  39. 31 8
      lib/ui/notification/update/change_log_page.dart
  40. 1 0
      lib/ui/viewer/file/file_details_widget.dart
  41. 8 0
      lib/ui/viewer/file/video_widget_new.dart
  42. 3 0
      lib/ui/viewer/file_details/albums_item_widget.dart
  43. 2 2
      lib/ui/viewer/file_details/file_properties_item_widget.dart
  44. 227 1
      lib/ui/viewer/file_details/location_tags_widget.dart
  45. 14 19
      lib/ui/viewer/gallery/component/group/lazy_group_gallery.dart
  46. 60 0
      lib/ui/viewer/gallery/gallery_app_bar_widget.dart
  47. 39 1
      lib/utils/exif_util.dart
  48. 1 1
      pubspec.yaml

+ 12 - 0
CHANGELOG.md

@@ -1,5 +1,17 @@
 # CHANGELOG
 # CHANGELOG
 
 
+## v0.8.54
+
+### Added
+* #### Map View ✨
+
+    You can now view the location where a photo was clicked. Open a photo and tap the Info button to view its place on the map!
+
+* #### Bug Fixes
+
+    Many a bugs were squashed in this release. If you run into any, please write to team@ente.io, or let us know on Discord! 🙏
+
+
 
 
 ## v0.7.118
 ## v0.7.118
 
 

+ 3 - 0
lib/events/pause_video_event.dart

@@ -0,0 +1,3 @@
+import "package:photos/events/event.dart";
+
+class PauseVideoEvent extends Event {}

+ 50 - 0
lib/gateways/cast_gw.dart

@@ -0,0 +1,50 @@
+import "package:dio/dio.dart";
+
+class CastGateway {
+  final Dio _enteDio;
+
+  CastGateway(this._enteDio);
+
+  Future<String?> getPublicKey(String deviceCode) async {
+    try {
+      final response = await _enteDio.get(
+        "/cast/device-info/$deviceCode",
+      );
+      return response.data["publicKey"];
+    } catch (e) {
+      if (e is DioError &&
+          e.response != null &&
+          e.response!.statusCode == 404) {
+        return null;
+      }
+      rethrow;
+    }
+  }
+
+  Future<void> publishCastPayload(
+    String code,
+    String castPayload,
+    int collectionID,
+    String castToken,
+  ) {
+    return _enteDio.post(
+      "/cast/cast-data/",
+      data: {
+        "deviceCode": code,
+        "encPayload": castPayload,
+        "collectionID": collectionID,
+        "castToken": castToken,
+      },
+    );
+  }
+
+  Future<void> revokeAllTokens() async {
+    try {
+      await _enteDio.delete(
+        "/cast/revoke-all-tokens/",
+      );
+    } catch (e) {
+      // swallow error
+    }
+  }
+}

+ 1 - 0
lib/generated/intl/messages_cs.dart

@@ -34,6 +34,7 @@ class MessageLookup extends MessageLookupByLibrary {
             MessageLookupByLibrary.simpleMessage(
             MessageLookupByLibrary.simpleMessage(
                 "Edits to location will only be seen within Ente"),
                 "Edits to location will only be seen within Ente"),
         "fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
         "fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
+        "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
         "modifyYourQueryOrTrySearchingFor":
         "modifyYourQueryOrTrySearchingFor":
             MessageLookupByLibrary.simpleMessage(
             MessageLookupByLibrary.simpleMessage(
                 "Modify your query, or try searching for"),
                 "Modify your query, or try searching for"),

+ 2 - 1
lib/generated/intl/messages_de.dart

@@ -815,6 +815,7 @@ class MessageLookup extends MessageLookupByLibrary {
                 "Elemente zeigen die Anzahl der Tage bis zum dauerhaften Löschen an"),
                 "Elemente zeigen die Anzahl der Tage bis zum dauerhaften Löschen an"),
         "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage(
         "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage(
             "Ausgewählte Elemente werden aus diesem Album entfernt"),
             "Ausgewählte Elemente werden aus diesem Album entfernt"),
+        "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
         "keepPhotos": MessageLookupByLibrary.simpleMessage("Fotos behalten"),
         "keepPhotos": MessageLookupByLibrary.simpleMessage("Fotos behalten"),
         "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"),
         "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"),
         "kindlyHelpUsWithThisInformation":
         "kindlyHelpUsWithThisInformation":
@@ -845,7 +846,7 @@ class MessageLookup extends MessageLookupByLibrary {
         "loadMessage1": MessageLookupByLibrary.simpleMessage(
         "loadMessage1": MessageLookupByLibrary.simpleMessage(
             "Du kannst dein Abonnement mit deiner Familie teilen"),
             "Du kannst dein Abonnement mit deiner Familie teilen"),
         "loadMessage2": MessageLookupByLibrary.simpleMessage(
         "loadMessage2": MessageLookupByLibrary.simpleMessage(
-            "Wir haben bereits mehr als 10 Millionen Erinnerungsstücke gesichert"),
+            "Wir haben bereits mehr als 30 Millionen Erinnerungsstücke gesichert"),
         "loadMessage3": MessageLookupByLibrary.simpleMessage(
         "loadMessage3": MessageLookupByLibrary.simpleMessage(
             "Wir behalten 3 Kopien Ihrer Daten, eine in einem unterirdischen Schutzbunker"),
             "Wir behalten 3 Kopien Ihrer Daten, eine in einem unterirdischen Schutzbunker"),
         "loadMessage4": MessageLookupByLibrary.simpleMessage(
         "loadMessage4": MessageLookupByLibrary.simpleMessage(

+ 10 - 1
lib/generated/intl/messages_en.dart

@@ -380,6 +380,8 @@ class MessageLookup extends MessageLookupByLibrary {
         "cannotAddMorePhotosAfterBecomingViewer": m7,
         "cannotAddMorePhotosAfterBecomingViewer": m7,
         "cannotDeleteSharedFiles":
         "cannotDeleteSharedFiles":
             MessageLookupByLibrary.simpleMessage("Cannot delete shared files"),
             MessageLookupByLibrary.simpleMessage("Cannot delete shared files"),
+        "castInstruction": MessageLookupByLibrary.simpleMessage(
+            "Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV."),
         "centerPoint": MessageLookupByLibrary.simpleMessage("Center point"),
         "centerPoint": MessageLookupByLibrary.simpleMessage("Center point"),
         "changeEmail": MessageLookupByLibrary.simpleMessage("Change email"),
         "changeEmail": MessageLookupByLibrary.simpleMessage("Change email"),
         "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage(
         "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage(
@@ -552,10 +554,14 @@ class MessageLookup extends MessageLookupByLibrary {
         "details": MessageLookupByLibrary.simpleMessage("Details"),
         "details": MessageLookupByLibrary.simpleMessage("Details"),
         "devAccountChanged": MessageLookupByLibrary.simpleMessage(
         "devAccountChanged": MessageLookupByLibrary.simpleMessage(
             "The developer account we use to publish ente on App Store has changed. Because of this, you will need to login again.\n\nOur apologies for the inconvenience, but this was unavoidable."),
             "The developer account we use to publish ente on App Store has changed. Because of this, you will need to login again.\n\nOur apologies for the inconvenience, but this was unavoidable."),
+        "deviceCodeHint":
+            MessageLookupByLibrary.simpleMessage("Enter the code"),
         "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage(
         "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage(
             "Files added to this device album will automatically get uploaded to ente."),
             "Files added to this device album will automatically get uploaded to ente."),
         "deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
         "deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
             "Disable the device screen lock when ente is in the foreground and there is a backup in progress. This is normally not needed, but may help big uploads and initial imports of large libraries complete faster."),
             "Disable the device screen lock when ente is in the foreground and there is a backup in progress. This is normally not needed, but may help big uploads and initial imports of large libraries complete faster."),
+        "deviceNotFound":
+            MessageLookupByLibrary.simpleMessage("Device not found"),
         "didYouKnow": MessageLookupByLibrary.simpleMessage("Did you know?"),
         "didYouKnow": MessageLookupByLibrary.simpleMessage("Did you know?"),
         "disableAutoLock":
         "disableAutoLock":
             MessageLookupByLibrary.simpleMessage("Disable auto lock"),
             MessageLookupByLibrary.simpleMessage("Disable auto lock"),
@@ -784,6 +790,7 @@ class MessageLookup extends MessageLookupByLibrary {
                 "Items show the number of days remaining before permanent deletion"),
                 "Items show the number of days remaining before permanent deletion"),
         "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage(
         "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage(
             "Selected items will be removed from this album"),
             "Selected items will be removed from this album"),
+        "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
         "keepPhotos": MessageLookupByLibrary.simpleMessage("Keep Photos"),
         "keepPhotos": MessageLookupByLibrary.simpleMessage("Keep Photos"),
         "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"),
         "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"),
         "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage(
         "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage(
@@ -811,7 +818,7 @@ class MessageLookup extends MessageLookupByLibrary {
         "loadMessage1": MessageLookupByLibrary.simpleMessage(
         "loadMessage1": MessageLookupByLibrary.simpleMessage(
             "You can share your subscription with your family"),
             "You can share your subscription with your family"),
         "loadMessage2": MessageLookupByLibrary.simpleMessage(
         "loadMessage2": MessageLookupByLibrary.simpleMessage(
-            "We have preserved over 10 million memories so far"),
+            "We have preserved over 30 million memories so far"),
         "loadMessage3": MessageLookupByLibrary.simpleMessage(
         "loadMessage3": MessageLookupByLibrary.simpleMessage(
             "We keep 3 copies of your data, one in an underground fallout shelter"),
             "We keep 3 copies of your data, one in an underground fallout shelter"),
         "loadMessage4": MessageLookupByLibrary.simpleMessage(
         "loadMessage4": MessageLookupByLibrary.simpleMessage(
@@ -946,6 +953,7 @@ class MessageLookup extends MessageLookupByLibrary {
             "Optional, as short as you like..."),
             "Optional, as short as you like..."),
         "orPickAnExistingOne":
         "orPickAnExistingOne":
             MessageLookupByLibrary.simpleMessage("Or pick an existing one"),
             MessageLookupByLibrary.simpleMessage("Or pick an existing one"),
+        "pair": MessageLookupByLibrary.simpleMessage("Pair"),
         "password": MessageLookupByLibrary.simpleMessage("Password"),
         "password": MessageLookupByLibrary.simpleMessage("Password"),
         "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage(
         "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage(
             "Password changed successfully"),
             "Password changed successfully"),
@@ -980,6 +988,7 @@ class MessageLookup extends MessageLookupByLibrary {
         "pickCenterPoint":
         "pickCenterPoint":
             MessageLookupByLibrary.simpleMessage("Pick center point"),
             MessageLookupByLibrary.simpleMessage("Pick center point"),
         "pinAlbum": MessageLookupByLibrary.simpleMessage("Pin album"),
         "pinAlbum": MessageLookupByLibrary.simpleMessage("Pin album"),
+        "playOnTv": MessageLookupByLibrary.simpleMessage("Play album on TV"),
         "playStoreFreeTrialValidTill": m37,
         "playStoreFreeTrialValidTill": m37,
         "playstoreSubscription":
         "playstoreSubscription":
             MessageLookupByLibrary.simpleMessage("PlayStore subscription"),
             MessageLookupByLibrary.simpleMessage("PlayStore subscription"),

+ 2 - 1
lib/generated/intl/messages_es.dart

@@ -703,6 +703,7 @@ class MessageLookup extends MessageLookupByLibrary {
                 "Los artículos muestran el número de días restantes antes de ser borrados permanente"),
                 "Los artículos muestran el número de días restantes antes de ser borrados permanente"),
         "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage(
         "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage(
             "Los elementos seleccionados serán removidos de este álbum"),
             "Los elementos seleccionados serán removidos de este álbum"),
+        "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
         "keepPhotos":
         "keepPhotos":
             MessageLookupByLibrary.simpleMessage("Conservar las fotos"),
             MessageLookupByLibrary.simpleMessage("Conservar las fotos"),
         "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"),
         "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"),
@@ -733,7 +734,7 @@ class MessageLookup extends MessageLookupByLibrary {
         "loadMessage1": MessageLookupByLibrary.simpleMessage(
         "loadMessage1": MessageLookupByLibrary.simpleMessage(
             "Puedes compartir tu suscripción con tu familia"),
             "Puedes compartir tu suscripción con tu familia"),
         "loadMessage2": MessageLookupByLibrary.simpleMessage(
         "loadMessage2": MessageLookupByLibrary.simpleMessage(
-            "Hasta ahora hemos conservado más de 10 millones de recuerdos"),
+            "Hasta ahora hemos conservado más de 30 millones de recuerdos"),
         "loadMessage3": MessageLookupByLibrary.simpleMessage(
         "loadMessage3": MessageLookupByLibrary.simpleMessage(
             "Guardamos 3 copias de sus datos, una en un refugio subterráneo"),
             "Guardamos 3 copias de sus datos, una en un refugio subterráneo"),
         "loadMessage4": MessageLookupByLibrary.simpleMessage(
         "loadMessage4": MessageLookupByLibrary.simpleMessage(

+ 2 - 1
lib/generated/intl/messages_fr.dart

@@ -811,6 +811,7 @@ class MessageLookup extends MessageLookupByLibrary {
                 "Les éléments montrent le nombre de jours restants avant la suppression définitive"),
                 "Les éléments montrent le nombre de jours restants avant la suppression définitive"),
         "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage(
         "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage(
             "Les éléments sélectionnés seront supprimés de cet album"),
             "Les éléments sélectionnés seront supprimés de cet album"),
+        "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
         "keepPhotos":
         "keepPhotos":
             MessageLookupByLibrary.simpleMessage("Conserver les photos"),
             MessageLookupByLibrary.simpleMessage("Conserver les photos"),
         "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"),
         "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"),
@@ -843,7 +844,7 @@ class MessageLookup extends MessageLookupByLibrary {
         "loadMessage1": MessageLookupByLibrary.simpleMessage(
         "loadMessage1": MessageLookupByLibrary.simpleMessage(
             "Vous pouvez partager votre abonnement avec votre famille"),
             "Vous pouvez partager votre abonnement avec votre famille"),
         "loadMessage2": MessageLookupByLibrary.simpleMessage(
         "loadMessage2": MessageLookupByLibrary.simpleMessage(
-            "Nous avons conservé plus de 10 millions de souvenirs jusqu\'à présent"),
+            "Nous avons conservé plus de 30 millions de souvenirs jusqu\'à présent"),
         "loadMessage3": MessageLookupByLibrary.simpleMessage(
         "loadMessage3": MessageLookupByLibrary.simpleMessage(
             "Nous conservons 3 copies de vos données, l\'une dans un abri anti-atomique"),
             "Nous conservons 3 copies de vos données, l\'une dans un abri anti-atomique"),
         "loadMessage4": MessageLookupByLibrary.simpleMessage(
         "loadMessage4": MessageLookupByLibrary.simpleMessage(

+ 2 - 1
lib/generated/intl/messages_it.dart

@@ -780,6 +780,7 @@ class MessageLookup extends MessageLookupByLibrary {
                 "Gli elementi mostrano il numero di giorni rimanenti prima della cancellazione permanente"),
                 "Gli elementi mostrano il numero di giorni rimanenti prima della cancellazione permanente"),
         "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage(
         "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage(
             "Gli elementi selezionati saranno rimossi da questo album"),
             "Gli elementi selezionati saranno rimossi da questo album"),
+        "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
         "keepPhotos": MessageLookupByLibrary.simpleMessage("Mantieni foto"),
         "keepPhotos": MessageLookupByLibrary.simpleMessage("Mantieni foto"),
         "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"),
         "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"),
         "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage(
         "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage(
@@ -810,7 +811,7 @@ class MessageLookup extends MessageLookupByLibrary {
         "loadMessage1": MessageLookupByLibrary.simpleMessage(
         "loadMessage1": MessageLookupByLibrary.simpleMessage(
             "Puoi condividere il tuo abbonamento con la tua famiglia"),
             "Puoi condividere il tuo abbonamento con la tua famiglia"),
         "loadMessage2": MessageLookupByLibrary.simpleMessage(
         "loadMessage2": MessageLookupByLibrary.simpleMessage(
-            "Fino ad oggi abbiamo conservato oltre 10 milioni di ricordi"),
+            "Fino ad oggi abbiamo conservato oltre 30 milioni di ricordi"),
         "loadMessage3": MessageLookupByLibrary.simpleMessage(
         "loadMessage3": MessageLookupByLibrary.simpleMessage(
             "Teniamo 3 copie dei tuoi dati, uno in un rifugio sotterraneo antiatomico"),
             "Teniamo 3 copie dei tuoi dati, uno in un rifugio sotterraneo antiatomico"),
         "loadMessage4": MessageLookupByLibrary.simpleMessage(
         "loadMessage4": MessageLookupByLibrary.simpleMessage(

+ 1 - 0
lib/generated/intl/messages_ko.dart

@@ -34,6 +34,7 @@ class MessageLookup extends MessageLookupByLibrary {
             MessageLookupByLibrary.simpleMessage(
             MessageLookupByLibrary.simpleMessage(
                 "Edits to location will only be seen within Ente"),
                 "Edits to location will only be seen within Ente"),
         "fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
         "fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
+        "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
         "modifyYourQueryOrTrySearchingFor":
         "modifyYourQueryOrTrySearchingFor":
             MessageLookupByLibrary.simpleMessage(
             MessageLookupByLibrary.simpleMessage(
                 "Modify your query, or try searching for"),
                 "Modify your query, or try searching for"),

+ 10 - 0
lib/generated/intl/messages_nl.dart

@@ -392,6 +392,8 @@ class MessageLookup extends MessageLookupByLibrary {
         "cannotAddMorePhotosAfterBecomingViewer": m7,
         "cannotAddMorePhotosAfterBecomingViewer": m7,
         "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage(
         "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage(
             "Kan gedeelde bestanden niet verwijderen"),
             "Kan gedeelde bestanden niet verwijderen"),
+        "castInstruction": MessageLookupByLibrary.simpleMessage(
+            "Bezoek cast.ente.io op het apparaat dat u wilt koppelen.\n\nVoer de code hieronder in om het album op uw TV af te spelen."),
         "centerPoint": MessageLookupByLibrary.simpleMessage("Middelpunt"),
         "centerPoint": MessageLookupByLibrary.simpleMessage("Middelpunt"),
         "changeEmail": MessageLookupByLibrary.simpleMessage("E-mail wijzigen"),
         "changeEmail": MessageLookupByLibrary.simpleMessage("E-mail wijzigen"),
         "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage(
         "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage(
@@ -569,10 +571,14 @@ class MessageLookup extends MessageLookupByLibrary {
         "details": MessageLookupByLibrary.simpleMessage("Details"),
         "details": MessageLookupByLibrary.simpleMessage("Details"),
         "devAccountChanged": MessageLookupByLibrary.simpleMessage(
         "devAccountChanged": MessageLookupByLibrary.simpleMessage(
             "Het ontwikkelaarsaccount dat we gebruiken om te publiceren in de App Store is veranderd. Daarom moet je opnieuw inloggen.\n\nOnze excuses voor het ongemak, helaas was dit onvermijdelijk."),
             "Het ontwikkelaarsaccount dat we gebruiken om te publiceren in de App Store is veranderd. Daarom moet je opnieuw inloggen.\n\nOnze excuses voor het ongemak, helaas was dit onvermijdelijk."),
+        "deviceCodeHint":
+            MessageLookupByLibrary.simpleMessage("Voer de code in"),
         "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage(
         "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage(
             "Bestanden toegevoegd aan dit album van dit apparaat zullen automatisch geüpload worden naar ente."),
             "Bestanden toegevoegd aan dit album van dit apparaat zullen automatisch geüpload worden naar ente."),
         "deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
         "deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
             "Schakel de schermvergrendeling van het apparaat uit wanneer ente op de voorgrond is en er een back-up aan de gang is. Dit is normaal gesproken niet nodig, maar kan grote uploads en initiële imports van grote mappen sneller laten verlopen."),
             "Schakel de schermvergrendeling van het apparaat uit wanneer ente op de voorgrond is en er een back-up aan de gang is. Dit is normaal gesproken niet nodig, maar kan grote uploads en initiële imports van grote mappen sneller laten verlopen."),
+        "deviceNotFound":
+            MessageLookupByLibrary.simpleMessage("Apparaat niet gevonden"),
         "didYouKnow": MessageLookupByLibrary.simpleMessage("Wist u dat?"),
         "didYouKnow": MessageLookupByLibrary.simpleMessage("Wist u dat?"),
         "disableAutoLock": MessageLookupByLibrary.simpleMessage(
         "disableAutoLock": MessageLookupByLibrary.simpleMessage(
             "Automatisch vergrendelen uitschakelen"),
             "Automatisch vergrendelen uitschakelen"),
@@ -816,6 +822,7 @@ class MessageLookup extends MessageLookupByLibrary {
                 "Bestanden tonen het aantal resterende dagen voordat ze permanent worden verwijderd"),
                 "Bestanden tonen het aantal resterende dagen voordat ze permanent worden verwijderd"),
         "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage(
         "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage(
             "Geselecteerde items zullen worden verwijderd uit dit album"),
             "Geselecteerde items zullen worden verwijderd uit dit album"),
+        "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
         "keepPhotos": MessageLookupByLibrary.simpleMessage("Foto\'s behouden"),
         "keepPhotos": MessageLookupByLibrary.simpleMessage("Foto\'s behouden"),
         "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"),
         "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"),
         "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage(
         "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage(
@@ -987,6 +994,7 @@ class MessageLookup extends MessageLookupByLibrary {
             "Optioneel, zo kort als je wilt..."),
             "Optioneel, zo kort als je wilt..."),
         "orPickAnExistingOne":
         "orPickAnExistingOne":
             MessageLookupByLibrary.simpleMessage("Of kies een bestaande"),
             MessageLookupByLibrary.simpleMessage("Of kies een bestaande"),
+        "pair": MessageLookupByLibrary.simpleMessage("Koppelen"),
         "password": MessageLookupByLibrary.simpleMessage("Wachtwoord"),
         "password": MessageLookupByLibrary.simpleMessage("Wachtwoord"),
         "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage(
         "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage(
             "Wachtwoord succesvol aangepast"),
             "Wachtwoord succesvol aangepast"),
@@ -1025,6 +1033,8 @@ class MessageLookup extends MessageLookupByLibrary {
             MessageLookupByLibrary.simpleMessage("Kies middelpunt"),
             MessageLookupByLibrary.simpleMessage("Kies middelpunt"),
         "pinAlbum":
         "pinAlbum":
             MessageLookupByLibrary.simpleMessage("Album bovenaan vastzetten"),
             MessageLookupByLibrary.simpleMessage("Album bovenaan vastzetten"),
+        "playOnTv":
+            MessageLookupByLibrary.simpleMessage("Album afspelen op TV"),
         "playStoreFreeTrialValidTill": m37,
         "playStoreFreeTrialValidTill": m37,
         "playstoreSubscription":
         "playstoreSubscription":
             MessageLookupByLibrary.simpleMessage("PlayStore abonnement"),
             MessageLookupByLibrary.simpleMessage("PlayStore abonnement"),

+ 1 - 0
lib/generated/intl/messages_no.dart

@@ -54,6 +54,7 @@ class MessageLookup extends MessageLookupByLibrary {
         "fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
         "fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
         "invalidEmailAddress":
         "invalidEmailAddress":
             MessageLookupByLibrary.simpleMessage("Ugyldig e-postadresse"),
             MessageLookupByLibrary.simpleMessage("Ugyldig e-postadresse"),
+        "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
         "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage(
         "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage(
             "Vær vennlig og hjelp oss med denne informasjonen"),
             "Vær vennlig og hjelp oss med denne informasjonen"),
         "modifyYourQueryOrTrySearchingFor":
         "modifyYourQueryOrTrySearchingFor":

+ 1 - 0
lib/generated/intl/messages_pl.dart

@@ -113,6 +113,7 @@ class MessageLookup extends MessageLookupByLibrary {
             "Nieprawidłowy klucz odzyskiwania"),
             "Nieprawidłowy klucz odzyskiwania"),
         "invalidEmailAddress":
         "invalidEmailAddress":
             MessageLookupByLibrary.simpleMessage("Nieprawidłowy adres e-mail"),
             MessageLookupByLibrary.simpleMessage("Nieprawidłowy adres e-mail"),
+        "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
         "kindlyHelpUsWithThisInformation":
         "kindlyHelpUsWithThisInformation":
             MessageLookupByLibrary.simpleMessage("Pomóż nam z tą informacją"),
             MessageLookupByLibrary.simpleMessage("Pomóż nam z tą informacją"),
         "logInLabel": MessageLookupByLibrary.simpleMessage("Zaloguj się"),
         "logInLabel": MessageLookupByLibrary.simpleMessage("Zaloguj się"),

+ 1 - 0
lib/generated/intl/messages_pt.dart

@@ -252,6 +252,7 @@ class MessageLookup extends MessageLookupByLibrary {
             MessageLookupByLibrary.simpleMessage("Convide seus amigos"),
             MessageLookupByLibrary.simpleMessage("Convide seus amigos"),
         "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage(
         "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage(
             "Os itens selecionados serão removidos deste álbum"),
             "Os itens selecionados serão removidos deste álbum"),
+        "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
         "keepPhotos": MessageLookupByLibrary.simpleMessage("Manter fotos"),
         "keepPhotos": MessageLookupByLibrary.simpleMessage("Manter fotos"),
         "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage(
         "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage(
             "Ajude-nos com esta informação"),
             "Ajude-nos com esta informação"),

+ 7 - 0
lib/generated/intl/messages_zh.dart

@@ -336,6 +336,8 @@ class MessageLookup extends MessageLookupByLibrary {
         "cannotAddMorePhotosAfterBecomingViewer": m7,
         "cannotAddMorePhotosAfterBecomingViewer": m7,
         "cannotDeleteSharedFiles":
         "cannotDeleteSharedFiles":
             MessageLookupByLibrary.simpleMessage("无法删除共享文件"),
             MessageLookupByLibrary.simpleMessage("无法删除共享文件"),
+        "castInstruction": MessageLookupByLibrary.simpleMessage(
+            "在您要配对的设备上访问 cast.ente.io。\n输入下面的代码即可在电视上播放相册。"),
         "centerPoint": MessageLookupByLibrary.simpleMessage("中心点"),
         "centerPoint": MessageLookupByLibrary.simpleMessage("中心点"),
         "changeEmail": MessageLookupByLibrary.simpleMessage("修改邮箱"),
         "changeEmail": MessageLookupByLibrary.simpleMessage("修改邮箱"),
         "changeLocationOfSelectedItems":
         "changeLocationOfSelectedItems":
@@ -468,10 +470,12 @@ class MessageLookup extends MessageLookupByLibrary {
         "details": MessageLookupByLibrary.simpleMessage("详情"),
         "details": MessageLookupByLibrary.simpleMessage("详情"),
         "devAccountChanged": MessageLookupByLibrary.simpleMessage(
         "devAccountChanged": MessageLookupByLibrary.simpleMessage(
             "我们用于在 App Store 上发布 ente 的开发者账户已更改。 因此,您将需要重新登录。\n\n对于给您带来的不便,我们深表歉意,但这是不可避免的。"),
             "我们用于在 App Store 上发布 ente 的开发者账户已更改。 因此,您将需要重新登录。\n\n对于给您带来的不便,我们深表歉意,但这是不可避免的。"),
+        "deviceCodeHint": MessageLookupByLibrary.simpleMessage("输入代码"),
         "deviceFilesAutoUploading":
         "deviceFilesAutoUploading":
             MessageLookupByLibrary.simpleMessage("添加到此设备相册的文件将自动上传到 ente。"),
             MessageLookupByLibrary.simpleMessage("添加到此设备相册的文件将自动上传到 ente。"),
         "deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
         "deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
             "当 ente 在前台并且正在进行备份时禁用设备屏幕锁定。 这通常不需要,但可以帮助大型库的大上传和初始导入更快地完成。"),
             "当 ente 在前台并且正在进行备份时禁用设备屏幕锁定。 这通常不需要,但可以帮助大型库的大上传和初始导入更快地完成。"),
+        "deviceNotFound": MessageLookupByLibrary.simpleMessage("未发现设备"),
         "didYouKnow": MessageLookupByLibrary.simpleMessage("您知道吗?"),
         "didYouKnow": MessageLookupByLibrary.simpleMessage("您知道吗?"),
         "disableAutoLock": MessageLookupByLibrary.simpleMessage("禁用自动锁定"),
         "disableAutoLock": MessageLookupByLibrary.simpleMessage("禁用自动锁定"),
         "disableDownloadWarningBody":
         "disableDownloadWarningBody":
@@ -662,6 +666,7 @@ class MessageLookup extends MessageLookupByLibrary {
             MessageLookupByLibrary.simpleMessage("项目显示永久删除前剩余的天数"),
             MessageLookupByLibrary.simpleMessage("项目显示永久删除前剩余的天数"),
         "itemsWillBeRemovedFromAlbum":
         "itemsWillBeRemovedFromAlbum":
             MessageLookupByLibrary.simpleMessage("所选项目将从此相册中移除"),
             MessageLookupByLibrary.simpleMessage("所选项目将从此相册中移除"),
+        "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
         "keepPhotos": MessageLookupByLibrary.simpleMessage("保留照片"),
         "keepPhotos": MessageLookupByLibrary.simpleMessage("保留照片"),
         "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("公里"),
         "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("公里"),
         "kindlyHelpUsWithThisInformation":
         "kindlyHelpUsWithThisInformation":
@@ -805,6 +810,7 @@ class MessageLookup extends MessageLookupByLibrary {
             MessageLookupByLibrary.simpleMessage("可选的,按您喜欢的短语..."),
             MessageLookupByLibrary.simpleMessage("可选的,按您喜欢的短语..."),
         "orPickAnExistingOne":
         "orPickAnExistingOne":
             MessageLookupByLibrary.simpleMessage("或者选择一个现有的"),
             MessageLookupByLibrary.simpleMessage("或者选择一个现有的"),
+        "pair": MessageLookupByLibrary.simpleMessage("配对"),
         "password": MessageLookupByLibrary.simpleMessage("密码"),
         "password": MessageLookupByLibrary.simpleMessage("密码"),
         "passwordChangedSuccessfully":
         "passwordChangedSuccessfully":
             MessageLookupByLibrary.simpleMessage("密码修改成功"),
             MessageLookupByLibrary.simpleMessage("密码修改成功"),
@@ -832,6 +838,7 @@ class MessageLookup extends MessageLookupByLibrary {
             MessageLookupByLibrary.simpleMessage("您添加的照片将从相册中移除"),
             MessageLookupByLibrary.simpleMessage("您添加的照片将从相册中移除"),
         "pickCenterPoint": MessageLookupByLibrary.simpleMessage("选择中心点"),
         "pickCenterPoint": MessageLookupByLibrary.simpleMessage("选择中心点"),
         "pinAlbum": MessageLookupByLibrary.simpleMessage("置顶相册"),
         "pinAlbum": MessageLookupByLibrary.simpleMessage("置顶相册"),
+        "playOnTv": MessageLookupByLibrary.simpleMessage("在电视上播放相册"),
         "playStoreFreeTrialValidTill": m37,
         "playStoreFreeTrialValidTill": m37,
         "playstoreSubscription":
         "playstoreSubscription":
             MessageLookupByLibrary.simpleMessage("PlayStore 订阅"),
             MessageLookupByLibrary.simpleMessage("PlayStore 订阅"),

+ 62 - 2
lib/generated/l10n.dart

@@ -6839,10 +6839,10 @@ class S {
     );
     );
   }
   }
 
 
-  /// `We have preserved over 10 million memories so far`
+  /// `We have preserved over 30 million memories so far`
   String get loadMessage2 {
   String get loadMessage2 {
     return Intl.message(
     return Intl.message(
-      'We have preserved over 10 million memories so far',
+      'We have preserved over 30 million memories so far',
       name: 'loadMessage2',
       name: 'loadMessage2',
       desc: '',
       desc: '',
       args: [],
       args: [],
@@ -8307,6 +8307,66 @@ class S {
       args: [],
       args: [],
     );
     );
   }
   }
+
+  /// `Play album on TV`
+  String get playOnTv {
+    return Intl.message(
+      'Play album on TV',
+      name: 'playOnTv',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Pair`
+  String get pair {
+    return Intl.message(
+      'Pair',
+      name: 'pair',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Device not found`
+  String get deviceNotFound {
+    return Intl.message(
+      'Device not found',
+      name: 'deviceNotFound',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV.`
+  String get castInstruction {
+    return Intl.message(
+      'Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV.',
+      name: 'castInstruction',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Enter the code`
+  String get deviceCodeHint {
+    return Intl.message(
+      'Enter the code',
+      name: 'deviceCodeHint',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Join Discord`
+  String get joinDiscord {
+    return Intl.message(
+      'Join Discord',
+      name: 'joinDiscord',
+      desc: '',
+      args: [],
+    );
+  }
 }
 }
 
 
 class AppLocalizationDelegate extends LocalizationsDelegate<S> {
 class AppLocalizationDelegate extends LocalizationsDelegate<S> {

+ 2 - 1
lib/l10n/intl_cs.arb

@@ -10,5 +10,6 @@
   "selectALocation": "Select a location",
   "selectALocation": "Select a location",
   "selectALocationFirst": "Select a location first",
   "selectALocationFirst": "Select a location first",
   "changeLocationOfSelectedItems": "Change location of selected items?",
   "changeLocationOfSelectedItems": "Change location of selected items?",
-  "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
+  "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
+  "joinDiscord": "Join Discord"
 }
 }

+ 3 - 2
lib/l10n/intl_de.arb

@@ -965,7 +965,7 @@
   "didYouKnow": "Schon gewusst?",
   "didYouKnow": "Schon gewusst?",
   "loadingMessage": "Fotos werden geladen...",
   "loadingMessage": "Fotos werden geladen...",
   "loadMessage1": "Du kannst dein Abonnement mit deiner Familie teilen",
   "loadMessage1": "Du kannst dein Abonnement mit deiner Familie teilen",
-  "loadMessage2": "Wir haben bereits mehr als 10 Millionen Erinnerungsstücke gesichert",
+  "loadMessage2": "Wir haben bereits mehr als 30 Millionen Erinnerungsstücke gesichert",
   "loadMessage3": "Wir behalten 3 Kopien Ihrer Daten, eine in einem unterirdischen Schutzbunker",
   "loadMessage3": "Wir behalten 3 Kopien Ihrer Daten, eine in einem unterirdischen Schutzbunker",
   "loadMessage4": "Alle unsere Apps sind Open-Source",
   "loadMessage4": "Alle unsere Apps sind Open-Source",
   "loadMessage5": "Unser Quellcode und unsere Kryptografie wurden extern geprüft",
   "loadMessage5": "Unser Quellcode und unsere Kryptografie wurden extern geprüft",
@@ -1178,5 +1178,6 @@
   "selectALocationFirst": "Wähle zuerst einen Standort",
   "selectALocationFirst": "Wähle zuerst einen Standort",
   "changeLocationOfSelectedItems": "Standort der gewählten Elemente ändern?",
   "changeLocationOfSelectedItems": "Standort der gewählten Elemente ändern?",
   "editsToLocationWillOnlyBeSeenWithinEnte": "Änderungen des Standorts werden nur in ente sichtbar sein",
   "editsToLocationWillOnlyBeSeenWithinEnte": "Änderungen des Standorts werden nur in ente sichtbar sein",
-  "cleanUncategorized": "Unkategorisiert leeren"
+  "cleanUncategorized": "Unkategorisiert leeren",
+  "joinDiscord": "Join Discord"
 }
 }

+ 9 - 3
lib/l10n/intl_en.arb

@@ -974,7 +974,7 @@
   "didYouKnow": "Did you know?",
   "didYouKnow": "Did you know?",
   "loadingMessage": "Loading your photos...",
   "loadingMessage": "Loading your photos...",
   "loadMessage1": "You can share your subscription with your family",
   "loadMessage1": "You can share your subscription with your family",
-  "loadMessage2": "We have preserved over 10 million memories so far",
+  "loadMessage2": "We have preserved over 30 million memories so far",
   "loadMessage3": "We keep 3 copies of your data, one in an underground fallout shelter",
   "loadMessage3": "We keep 3 copies of your data, one in an underground fallout shelter",
   "loadMessage4": "All our apps are open source",
   "loadMessage4": "All our apps are open source",
   "loadMessage5": "Our source code and cryptography have been externally audited",
   "loadMessage5": "Our source code and cryptography have been externally audited",
@@ -1187,5 +1187,11 @@
   "selectALocationFirst": "Select a location first",
   "selectALocationFirst": "Select a location first",
   "changeLocationOfSelectedItems": "Change location of selected items?",
   "changeLocationOfSelectedItems": "Change location of selected items?",
   "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
   "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
-  "cleanUncategorized": "Clean Uncategorized"
-}
+  "cleanUncategorized": "Clean Uncategorized",
+  "playOnTv": "Play album on TV",
+  "pair": "Pair",
+  "deviceNotFound": "Device not found",
+  "castInstruction": "Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV.",
+  "deviceCodeHint": "Enter the code",
+  "joinDiscord": "Join Discord"
+}

+ 3 - 2
lib/l10n/intl_es.arb

@@ -897,7 +897,7 @@
   "didYouKnow": "¿Sabías que?",
   "didYouKnow": "¿Sabías que?",
   "loadingMessage": "Cargando tus fotos...",
   "loadingMessage": "Cargando tus fotos...",
   "loadMessage1": "Puedes compartir tu suscripción con tu familia",
   "loadMessage1": "Puedes compartir tu suscripción con tu familia",
-  "loadMessage2": "Hasta ahora hemos conservado más de 10 millones de recuerdos",
+  "loadMessage2": "Hasta ahora hemos conservado más de 30 millones de recuerdos",
   "loadMessage3": "Guardamos 3 copias de sus datos, una en un refugio subterráneo",
   "loadMessage3": "Guardamos 3 copias de sus datos, una en un refugio subterráneo",
   "loadMessage4": "Todas nuestras aplicaciones son de código abierto",
   "loadMessage4": "Todas nuestras aplicaciones son de código abierto",
   "loadMessage5": "Nuestro código fuente y criptografía han sido auditados externamente",
   "loadMessage5": "Nuestro código fuente y criptografía han sido auditados externamente",
@@ -973,5 +973,6 @@
   "selectALocation": "Select a location",
   "selectALocation": "Select a location",
   "selectALocationFirst": "Select a location first",
   "selectALocationFirst": "Select a location first",
   "changeLocationOfSelectedItems": "Change location of selected items?",
   "changeLocationOfSelectedItems": "Change location of selected items?",
-  "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
+  "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
+  "joinDiscord": "Join Discord"
 }
 }

+ 3 - 2
lib/l10n/intl_fr.arb

@@ -948,7 +948,7 @@
   "didYouKnow": "Le savais-tu ?",
   "didYouKnow": "Le savais-tu ?",
   "loadingMessage": "Chargement de vos photos...",
   "loadingMessage": "Chargement de vos photos...",
   "loadMessage1": "Vous pouvez partager votre abonnement avec votre famille",
   "loadMessage1": "Vous pouvez partager votre abonnement avec votre famille",
-  "loadMessage2": "Nous avons conservé plus de 10 millions de souvenirs jusqu'à présent",
+  "loadMessage2": "Nous avons conservé plus de 30 millions de souvenirs jusqu'à présent",
   "loadMessage3": "Nous conservons 3 copies de vos données, l'une dans un abri anti-atomique",
   "loadMessage3": "Nous conservons 3 copies de vos données, l'une dans un abri anti-atomique",
   "loadMessage4": "Toutes nos applications sont open source",
   "loadMessage4": "Toutes nos applications sont open source",
   "loadMessage5": "Notre code source et notre cryptographie ont été audités en externe",
   "loadMessage5": "Notre code source et notre cryptographie ont été audités en externe",
@@ -1154,5 +1154,6 @@
   "selectALocation": "Select a location",
   "selectALocation": "Select a location",
   "selectALocationFirst": "Select a location first",
   "selectALocationFirst": "Select a location first",
   "changeLocationOfSelectedItems": "Change location of selected items?",
   "changeLocationOfSelectedItems": "Change location of selected items?",
-  "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
+  "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
+  "joinDiscord": "Join Discord"
 }
 }

+ 3 - 2
lib/l10n/intl_it.arb

@@ -948,7 +948,7 @@
   "didYouKnow": "Lo sapevi che?",
   "didYouKnow": "Lo sapevi che?",
   "loadingMessage": "Caricando le tue foto...",
   "loadingMessage": "Caricando le tue foto...",
   "loadMessage1": "Puoi condividere il tuo abbonamento con la tua famiglia",
   "loadMessage1": "Puoi condividere il tuo abbonamento con la tua famiglia",
-  "loadMessage2": "Fino ad oggi abbiamo conservato oltre 10 milioni di ricordi",
+  "loadMessage2": "Fino ad oggi abbiamo conservato oltre 30 milioni di ricordi",
   "loadMessage3": "Teniamo 3 copie dei tuoi dati, uno in un rifugio sotterraneo antiatomico",
   "loadMessage3": "Teniamo 3 copie dei tuoi dati, uno in un rifugio sotterraneo antiatomico",
   "loadMessage4": "Tutte le nostre app sono open source",
   "loadMessage4": "Tutte le nostre app sono open source",
   "loadMessage5": "Il nostro codice sorgente e la crittografia hanno ricevuto audit esterni",
   "loadMessage5": "Il nostro codice sorgente e la crittografia hanno ricevuto audit esterni",
@@ -1116,5 +1116,6 @@
   "selectALocation": "Select a location",
   "selectALocation": "Select a location",
   "selectALocationFirst": "Select a location first",
   "selectALocationFirst": "Select a location first",
   "changeLocationOfSelectedItems": "Change location of selected items?",
   "changeLocationOfSelectedItems": "Change location of selected items?",
-  "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
+  "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
+  "joinDiscord": "Join Discord"
 }
 }

+ 2 - 1
lib/l10n/intl_ko.arb

@@ -10,5 +10,6 @@
   "selectALocation": "Select a location",
   "selectALocation": "Select a location",
   "selectALocationFirst": "Select a location first",
   "selectALocationFirst": "Select a location first",
   "changeLocationOfSelectedItems": "Change location of selected items?",
   "changeLocationOfSelectedItems": "Change location of selected items?",
-  "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
+  "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
+  "joinDiscord": "Join Discord"
 }
 }

+ 7 - 1
lib/l10n/intl_nl.arb

@@ -1187,5 +1187,11 @@
   "selectALocationFirst": "Selecteer eerst een locatie",
   "selectALocationFirst": "Selecteer eerst een locatie",
   "changeLocationOfSelectedItems": "Locatie van geselecteerde items wijzigen?",
   "changeLocationOfSelectedItems": "Locatie van geselecteerde items wijzigen?",
   "editsToLocationWillOnlyBeSeenWithinEnte": "Bewerkte locatie wordt alleen gezien binnen Ente",
   "editsToLocationWillOnlyBeSeenWithinEnte": "Bewerkte locatie wordt alleen gezien binnen Ente",
-  "cleanUncategorized": "Ongecategoriseerd opschonen"
+  "cleanUncategorized": "Ongecategoriseerd opschonen",
+  "playOnTv": "Album afspelen op TV",
+  "pair": "Koppelen",
+  "deviceNotFound": "Apparaat niet gevonden",
+  "castInstruction": "Bezoek cast.ente.io op het apparaat dat u wilt koppelen.\n\nVoer de code hieronder in om het album op uw TV af te spelen.",
+  "deviceCodeHint": "Voer de code in",
+  "joinDiscord": "Join Discord"
 }
 }

+ 2 - 1
lib/l10n/intl_no.arb

@@ -24,5 +24,6 @@
   "selectALocation": "Select a location",
   "selectALocation": "Select a location",
   "selectALocationFirst": "Select a location first",
   "selectALocationFirst": "Select a location first",
   "changeLocationOfSelectedItems": "Change location of selected items?",
   "changeLocationOfSelectedItems": "Change location of selected items?",
-  "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
+  "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
+  "joinDiscord": "Join Discord"
 }
 }

+ 2 - 1
lib/l10n/intl_pl.arb

@@ -111,5 +111,6 @@
   "selectALocation": "Select a location",
   "selectALocation": "Select a location",
   "selectALocationFirst": "Select a location first",
   "selectALocationFirst": "Select a location first",
   "changeLocationOfSelectedItems": "Change location of selected items?",
   "changeLocationOfSelectedItems": "Change location of selected items?",
-  "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
+  "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
+  "joinDiscord": "Join Discord"
 }
 }

+ 2 - 1
lib/l10n/intl_pt.arb

@@ -277,5 +277,6 @@
   "selectALocation": "Select a location",
   "selectALocation": "Select a location",
   "selectALocationFirst": "Select a location first",
   "selectALocationFirst": "Select a location first",
   "changeLocationOfSelectedItems": "Change location of selected items?",
   "changeLocationOfSelectedItems": "Change location of selected items?",
-  "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente"
+  "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
+  "joinDiscord": "Join Discord"
 }
 }

+ 7 - 1
lib/l10n/intl_zh.arb

@@ -1187,5 +1187,11 @@
   "selectALocationFirst": "首先选择一个位置",
   "selectALocationFirst": "首先选择一个位置",
   "changeLocationOfSelectedItems": "确定要更改所选项目的位置吗?",
   "changeLocationOfSelectedItems": "确定要更改所选项目的位置吗?",
   "editsToLocationWillOnlyBeSeenWithinEnte": "对位置的编辑只能在 Ente 内看到",
   "editsToLocationWillOnlyBeSeenWithinEnte": "对位置的编辑只能在 Ente 内看到",
-  "cleanUncategorized": "清除未分类的"
+  "cleanUncategorized": "清除未分类的",
+  "playOnTv": "在电视上播放相册",
+  "pair": "配对",
+  "deviceNotFound": "未发现设备",
+  "castInstruction": "在您要配对的设备上访问 cast.ente.io。\n输入下面的代码即可在电视上播放相册。",
+  "deviceCodeHint": "输入代码",
+  "joinDiscord": "Join Discord"
 }
 }

+ 17 - 0
lib/services/collections_service.dart

@@ -473,6 +473,23 @@ class CollectionsService {
     });
     });
   }
   }
 
 
+  String getCastData(
+    String castToken,
+    Collection collection,
+    String publicKey,
+  ) {
+    final String payload = jsonEncode({
+      "collectionID": collection.id,
+      "castToken": castToken,
+      "collectionKey": CryptoUtil.bin2base64(getCollectionKey(collection.id)),
+    });
+    final encPayload = CryptoUtil.sealSync(
+      CryptoUtil.base642bin(base64Encode(payload.codeUnits)),
+      CryptoUtil.base642bin(publicKey),
+    );
+    return CryptoUtil.bin2base64(encPayload);
+  }
+
   Future<List<User>> share(
   Future<List<User>> share(
     int collectionID,
     int collectionID,
     String email,
     String email,

+ 1 - 4
lib/services/location_service.dart

@@ -13,7 +13,6 @@ import "package:photos/models/local_entity_data.dart";
 import "package:photos/models/location/location.dart";
 import "package:photos/models/location/location.dart";
 import 'package:photos/models/location_tag/location_tag.dart';
 import 'package:photos/models/location_tag/location_tag.dart';
 import "package:photos/services/entity_service.dart";
 import "package:photos/services/entity_service.dart";
-import "package:photos/services/feature_flag_service.dart";
 import "package:photos/services/remote_assets_service.dart";
 import "package:photos/services/remote_assets_service.dart";
 import "package:shared_preferences/shared_preferences.dart";
 import "package:shared_preferences/shared_preferences.dart";
 
 
@@ -32,9 +31,7 @@ class LocationService {
 
 
   void init(SharedPreferences preferences) {
   void init(SharedPreferences preferences) {
     prefs = preferences;
     prefs = preferences;
-    if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
-      _loadCities();
-    }
+    _loadCities();
   }
   }
 
 
   Future<Iterable<LocalEntity<LocationTag>>> _getStoredLocationTags() async {
   Future<Iterable<LocalEntity<LocationTag>>> _getStoredLocationTags() async {

+ 1 - 1
lib/services/update_service.dart

@@ -16,7 +16,7 @@ class UpdateService {
   static final UpdateService instance = UpdateService._privateConstructor();
   static final UpdateService instance = UpdateService._privateConstructor();
   static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key";
   static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key";
   static const changeLogVersionKey = "update_change_log_key";
   static const changeLogVersionKey = "update_change_log_key";
-  static const currentChangeLogVersion = 13;
+  static const currentChangeLogVersion = 14;
 
 
   LatestVersionInfo? _latestVersion;
   LatestVersionInfo? _latestVersion;
   final _logger = Logger("UpdateService");
   final _logger = Logger("UpdateService");

+ 5 - 0
lib/ui/components/info_item_widget.dart

@@ -8,6 +8,7 @@ class InfoItemWidget extends StatelessWidget {
   final IconData leadingIcon;
   final IconData leadingIcon;
   final VoidCallback? editOnTap;
   final VoidCallback? editOnTap;
   final String? title;
   final String? title;
+  final Widget? endSection;
   final Future<List<Widget>> subtitleSection;
   final Future<List<Widget>> subtitleSection;
   final bool hasChipButtons;
   final bool hasChipButtons;
   final VoidCallback? onTap;
   final VoidCallback? onTap;
@@ -15,6 +16,7 @@ class InfoItemWidget extends StatelessWidget {
     required this.leadingIcon,
     required this.leadingIcon,
     this.editOnTap,
     this.editOnTap,
     this.title,
     this.title,
+    this.endSection,
     required this.subtitleSection,
     required this.subtitleSection,
     this.hasChipButtons = false,
     this.hasChipButtons = false,
     this.onTap,
     this.onTap,
@@ -70,6 +72,9 @@ class InfoItemWidget extends StatelessWidget {
         ),
         ),
       ),
       ),
     ]);
     ]);
+
+    endSection != null ? children.add(endSection!) : null;
+
     return Row(
     return Row(
       mainAxisAlignment: MainAxisAlignment.spaceBetween,
       mainAxisAlignment: MainAxisAlignment.spaceBetween,
       crossAxisAlignment: CrossAxisAlignment.start,
       crossAxisAlignment: CrossAxisAlignment.start,

+ 6 - 0
lib/ui/map/enable_map.dart

@@ -48,3 +48,9 @@ Future<bool> requestForMapEnable(BuildContext context) async {
   }
   }
   return false;
   return false;
 }
 }
+
+//For debugging.
+void disableMap() {
+  UserRemoteFlagService.instance
+      .setBoolValue(UserRemoteFlagService.mapEnabled, false);
+}

+ 12 - 4
lib/ui/map/map_marker.dart

@@ -2,20 +2,28 @@ import "package:flutter/material.dart";
 import "package:flutter_map/flutter_map.dart";
 import "package:flutter_map/flutter_map.dart";
 import "package:latlong2/latlong.dart";
 import "package:latlong2/latlong.dart";
 import "package:photos/ui/map/image_marker.dart";
 import "package:photos/ui/map/image_marker.dart";
+import "package:photos/ui/map/map_view.dart";
 import "package:photos/ui/map/marker_image.dart";
 import "package:photos/ui/map/marker_image.dart";
 
 
-Marker mapMarker(ImageMarker imageMarker, String key) {
+Marker mapMarker(
+  ImageMarker imageMarker,
+  String key, {
+  Size markerSize = MapView.defaultMarkerSize,
+}) {
   return Marker(
   return Marker(
+    //-6.5 is for taking in the height of the MarkerPointer
+    anchorPos: AnchorPos.exactly(Anchor(markerSize.height / 2, -6.5)),
     key: Key(key),
     key: Key(key),
-    width: 75,
-    height: 75,
+    width: markerSize.width,
+    height: markerSize.height,
     point: LatLng(
     point: LatLng(
       imageMarker.latitude,
       imageMarker.latitude,
       imageMarker.longitude,
       imageMarker.longitude,
     ),
     ),
     builder: (context) => MarkerImage(
     builder: (context) => MarkerImage(
       file: imageMarker.imageFile,
       file: imageMarker.imageFile,
-      seperator: 85,
+      seperator: (MapView.defaultMarkerSize.height + 10) -
+          (MapView.defaultMarkerSize.height - markerSize.height),
     ),
     ),
   );
   );
 }
 }

+ 68 - 42
lib/ui/map/map_screen.dart

@@ -2,6 +2,7 @@ import "dart:async";
 import "dart:isolate";
 import "dart:isolate";
 
 
 import "package:collection/collection.dart";
 import "package:collection/collection.dart";
+import "package:computer/computer.dart";
 import "package:flutter/foundation.dart";
 import "package:flutter/foundation.dart";
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_map/flutter_map.dart';
 import 'package:flutter_map/flutter_map.dart';
@@ -22,10 +23,14 @@ class MapScreen extends StatefulWidget {
   // Add a function parameter where the function returns a Future<List<File>>
   // Add a function parameter where the function returns a Future<List<File>>
 
 
   final Future<List<EnteFile>> Function() filesFutureFn;
   final Future<List<EnteFile>> Function() filesFutureFn;
+  final LatLng? center;
+  final double initialZoom;
 
 
   const MapScreen({
   const MapScreen({
     super.key,
     super.key,
     required this.filesFutureFn,
     required this.filesFutureFn,
+    this.center,
+    this.initialZoom = 4.5,
   });
   });
 
 
   @override
   @override
@@ -41,11 +46,10 @@ class _MapScreenState extends State<MapScreen> {
       StreamController<List<EnteFile>>.broadcast();
       StreamController<List<EnteFile>>.broadcast();
   MapController mapController = MapController();
   MapController mapController = MapController();
   bool isLoading = true;
   bool isLoading = true;
-  double initialZoom = 4.5;
   double maxZoom = 18.0;
   double maxZoom = 18.0;
   double minZoom = 2.8;
   double minZoom = 2.8;
   int debounceDuration = 500;
   int debounceDuration = 500;
-  LatLng center = const LatLng(46.7286, 4.8614);
+  late LatLng center;
   final Logger _logger = Logger("_MapScreenState");
   final Logger _logger = Logger("_MapScreenState");
   StreamSubscription? _mapMoveSubscription;
   StreamSubscription? _mapMoveSubscription;
   Isolate? isolate;
   Isolate? isolate;
@@ -67,6 +71,7 @@ class _MapScreenState extends State<MapScreen> {
 
 
   Future<void> initialize() async {
   Future<void> initialize() async {
     try {
     try {
+      center = widget.center ?? const LatLng(46.7286, 4.8614);
       allImages = await widget.filesFutureFn();
       allImages = await widget.filesFutureFn();
       unawaited(processFiles(allImages));
       unawaited(processFiles(allImages));
     } catch (e, s) {
     } catch (e, s) {
@@ -75,47 +80,25 @@ class _MapScreenState extends State<MapScreen> {
   }
   }
 
 
   Future<void> processFiles(List<EnteFile> files) async {
   Future<void> processFiles(List<EnteFile> files) async {
-    final List<ImageMarker> tempMarkers = [];
-    bool hasAnyLocation = false;
-    EnteFile? mostRecentFile;
-    for (var file in files) {
-      if (file.hasLocation) {
-        if (!Location.isValidRange(
-          latitude: file.location!.latitude!,
-          longitude: file.location!.longitude!,
-        )) {
-          _logger.warning(
-            'Skipping file with invalid location ${file.toString()}',
-          );
-          continue;
-        }
-        hasAnyLocation = true;
-        if (mostRecentFile == null) {
-          mostRecentFile = file;
-        } else {
-          if ((mostRecentFile.creationTime ?? 0) < (file.creationTime ?? 0)) {
-            mostRecentFile = file;
-          }
-        }
+    final result = await Computer.shared().compute(
+      _findRecentFileAndGenerateTempMarkers,
+      param: {"files": files, "center": widget.center},
+    );
 
 
-        tempMarkers.add(
-          ImageMarker(
-            latitude: file.location!.latitude!,
-            longitude: file.location!.longitude!,
-            imageFile: file,
-          ),
-        );
-      }
-    }
+    final EnteFile? mostRecentFile = result.$1;
+    final List<ImageMarker> tempMarkers = result.$2;
 
 
-    if (hasAnyLocation) {
-      center = LatLng(
-        mostRecentFile!.location!.latitude!,
-        mostRecentFile.location!.longitude!,
-      );
+    if (tempMarkers.isNotEmpty) {
+      center = widget.center ??
+          LatLng(
+            mostRecentFile!.location!.latitude!,
+            mostRecentFile.location!.longitude!,
+          );
 
 
       if (kDebugMode) {
       if (kDebugMode) {
-        debugPrint("Info for map: center $center, initialZoom $initialZoom");
+        debugPrint(
+          "Info for map: center $center, initialZoom ${widget.initialZoom}",
+        );
       }
       }
     } else {
     } else {
       showShortToast(context, S.of(context).noImagesWithLocation);
       showShortToast(context, S.of(context).noImagesWithLocation);
@@ -127,7 +110,7 @@ class _MapScreenState extends State<MapScreen> {
 
 
     mapController.move(
     mapController.move(
       center,
       center,
-      initialZoom,
+      widget.initialZoom,
     );
     );
 
 
     Timer(Duration(milliseconds: debounceDuration), () {
     Timer(Duration(milliseconds: debounceDuration), () {
@@ -163,6 +146,50 @@ class _MapScreenState extends State<MapScreen> {
     });
     });
   }
   }
 
 
+  static (EnteFile?, List<ImageMarker>) _findRecentFileAndGenerateTempMarkers(
+    Map<String, dynamic> args,
+  ) {
+    final Logger logger = Logger("_MapScreenState");
+    final files = args["files"] as List<EnteFile>;
+    final center = args["center"] as LatLng?;
+    final List<ImageMarker> tempMarkers = [];
+    EnteFile? mostRecentFile;
+
+    for (var file in files) {
+      if (file.hasLocation) {
+        if (!Location.isValidRange(
+          latitude: file.location!.latitude!,
+          longitude: file.location!.longitude!,
+        )) {
+          logger.warning(
+            'Skipping file with invalid location ${file.toString()}',
+          );
+          continue;
+        }
+
+        if (center == null) {
+          if (mostRecentFile == null) {
+            mostRecentFile = file;
+          } else {
+            if ((mostRecentFile.creationTime ?? 0) < (file.creationTime ?? 0)) {
+              mostRecentFile = file;
+            }
+          }
+        }
+
+        tempMarkers.add(
+          ImageMarker(
+            latitude: file.location!.latitude!,
+            longitude: file.location!.longitude!,
+            imageFile: file,
+          ),
+        );
+      }
+    }
+
+    return (mostRecentFile, tempMarkers);
+  }
+
   @pragma('vm:entry-point')
   @pragma('vm:entry-point')
   static void _calculateMarkersIsolate(MapIsolate message) async {
   static void _calculateMarkersIsolate(MapIsolate message) async {
     final bounds = message.bounds;
     final bounds = message.bounds;
@@ -211,10 +238,9 @@ class _MapScreenState extends State<MapScreen> {
                       imageMarkers: imageMarkers,
                       imageMarkers: imageMarkers,
                       updateVisibleImages: calculateVisibleMarkers,
                       updateVisibleImages: calculateVisibleMarkers,
                       center: center,
                       center: center,
-                      initialZoom: initialZoom,
+                      initialZoom: widget.initialZoom,
                       minZoom: minZoom,
                       minZoom: minZoom,
                       maxZoom: maxZoom,
                       maxZoom: maxZoom,
-                      debounceDuration: debounceDuration,
                       bottomSheetDraggableAreaHeight:
                       bottomSheetDraggableAreaHeight:
                           bottomSheetDraggableAreaHeight,
                           bottomSheetDraggableAreaHeight,
                     ),
                     ),

+ 71 - 46
lib/ui/map/map_view.dart

@@ -18,8 +18,13 @@ class MapView extends StatefulWidget {
   final double minZoom;
   final double minZoom;
   final double maxZoom;
   final double maxZoom;
   final double initialZoom;
   final double initialZoom;
-  final int debounceDuration;
   final double bottomSheetDraggableAreaHeight;
   final double bottomSheetDraggableAreaHeight;
+  final bool showControls;
+  final int interactiveFlags;
+  final VoidCallback? onTap;
+  final Size markerSize;
+  final MapAttributionOptions mapAttributionOptions;
+  static const defaultMarkerSize = Size(75, 75);
 
 
   const MapView({
   const MapView({
     Key? key,
     Key? key,
@@ -30,8 +35,12 @@ class MapView extends StatefulWidget {
     required this.minZoom,
     required this.minZoom,
     required this.maxZoom,
     required this.maxZoom,
     required this.initialZoom,
     required this.initialZoom,
-    required this.debounceDuration,
     required this.bottomSheetDraggableAreaHeight,
     required this.bottomSheetDraggableAreaHeight,
+    this.mapAttributionOptions = const MapAttributionOptions(),
+    this.markerSize = MapView.defaultMarkerSize,
+    this.onTap,
+    this.interactiveFlags = InteractiveFlag.all,
+    this.showControls = true,
   }) : super(key: key);
   }) : super(key: key);
 
 
   @override
   @override
@@ -71,6 +80,11 @@ class _MapViewState extends State<MapView> {
         FlutterMap(
         FlutterMap(
           mapController: widget.controller,
           mapController: widget.controller,
           options: MapOptions(
           options: MapOptions(
+            onTap: widget.onTap != null
+                ? (_, __) {
+                    widget.onTap!.call();
+                  }
+                : null,
             center: widget.center,
             center: widget.center,
             minZoom: widget.minZoom,
             minZoom: widget.minZoom,
             maxZoom: widget.maxZoom,
             maxZoom: widget.maxZoom,
@@ -85,13 +99,16 @@ class _MapViewState extends State<MapView> {
                 onChange(position.bounds!);
                 onChange(position.bounds!);
               }
               }
             },
             },
+            interactiveFlags: widget.interactiveFlags,
           ),
           ),
           nonRotatedChildren: [
           nonRotatedChildren: [
             Padding(
             Padding(
               padding: EdgeInsets.only(
               padding: EdgeInsets.only(
                 bottom: widget.bottomSheetDraggableAreaHeight,
                 bottom: widget.bottomSheetDraggableAreaHeight,
               ),
               ),
-              child: const OSMFranceTileAttributes(),
+              child: OSMFranceTileAttributes(
+                options: widget.mapAttributionOptions,
+              ),
             ),
             ),
           ],
           ],
           children: [
           children: [
@@ -101,7 +118,7 @@ class _MapViewState extends State<MapView> {
                 anchorPos: AnchorPos.align(AnchorAlign.top),
                 anchorPos: AnchorPos.align(AnchorAlign.top),
                 maxClusterRadius: 100,
                 maxClusterRadius: 100,
                 showPolygon: false,
                 showPolygon: false,
-                size: const Size(75, 75),
+                size: widget.markerSize,
                 fitBoundsOptions: const FitBoundsOptions(
                 fitBoundsOptions: const FitBoundsOptions(
                   padding: EdgeInsets.all(80),
                   padding: EdgeInsets.all(80),
                 ),
                 ),
@@ -133,47 +150,51 @@ class _MapViewState extends State<MapView> {
             ),
             ),
           ],
           ],
         ),
         ),
-        Positioned(
-          top: 4,
-          left: 10,
-          child: SafeArea(
-            child: MapButton(
-              icon: Icons.arrow_back,
-              onPressed: () {
-                Navigator.pop(context);
-              },
-              heroTag: 'back',
-            ),
-          ),
-        ),
-        Positioned(
-          bottom: widget.bottomSheetDraggableAreaHeight + 10,
-          right: 10,
-          child: Column(
-            children: [
-              MapButton(
-                icon: Icons.add,
-                onPressed: () {
-                  widget.controller.move(
-                    widget.controller.center,
-                    widget.controller.zoom + 1,
-                  );
-                },
-                heroTag: 'zoom-in',
-              ),
-              MapButton(
-                icon: Icons.remove,
-                onPressed: () {
-                  widget.controller.move(
-                    widget.controller.center,
-                    widget.controller.zoom - 1,
-                  );
-                },
-                heroTag: 'zoom-out',
-              ),
-            ],
-          ),
-        ),
+        widget.showControls
+            ? Positioned(
+                top: 4,
+                left: 10,
+                child: SafeArea(
+                  child: MapButton(
+                    icon: Icons.arrow_back,
+                    onPressed: () {
+                      Navigator.pop(context);
+                    },
+                    heroTag: 'back',
+                  ),
+                ),
+              )
+            : const SizedBox.shrink(),
+        widget.showControls
+            ? Positioned(
+                bottom: widget.bottomSheetDraggableAreaHeight + 10,
+                right: 10,
+                child: Column(
+                  children: [
+                    MapButton(
+                      icon: Icons.add,
+                      onPressed: () {
+                        widget.controller.move(
+                          widget.controller.center,
+                          widget.controller.zoom + 1,
+                        );
+                      },
+                      heroTag: 'zoom-in',
+                    ),
+                    MapButton(
+                      icon: Icons.remove,
+                      onPressed: () {
+                        widget.controller.move(
+                          widget.controller.center,
+                          widget.controller.zoom - 1,
+                        );
+                      },
+                      heroTag: 'zoom-out',
+                    ),
+                  ],
+                ),
+              )
+            : const SizedBox.shrink(),
       ],
       ],
     );
     );
   }
   }
@@ -181,7 +202,11 @@ class _MapViewState extends State<MapView> {
   List<Marker> _buildMakers() {
   List<Marker> _buildMakers() {
     return List<Marker>.generate(widget.imageMarkers.length, (index) {
     return List<Marker>.generate(widget.imageMarkers.length, (index) {
       final imageMarker = widget.imageMarkers[index];
       final imageMarker = widget.imageMarkers[index];
-      return mapMarker(imageMarker, index.toString());
+      return mapMarker(
+        imageMarker,
+        index.toString(),
+        markerSize: widget.markerSize,
+      );
     });
     });
   }
   }
 }
 }

+ 17 - 16
lib/ui/map/tile/attribution/map_attribution.dart

@@ -5,7 +5,9 @@ import "dart:async";
 import "package:flutter/material.dart";
 import "package:flutter/material.dart";
 import "package:flutter_map/plugin_api.dart";
 import "package:flutter_map/plugin_api.dart";
 import "package:photos/extensions/list.dart";
 import "package:photos/extensions/list.dart";
+import "package:photos/theme/colors.dart";
 import "package:photos/theme/ente_theme.dart";
 import "package:photos/theme/ente_theme.dart";
+import "package:photos/ui/components/buttons/icon_button_widget.dart";
 
 
 // Credit: This code is based on the Rich Attribution widget from the flutter_map
 // Credit: This code is based on the Rich Attribution widget from the flutter_map
 class MapAttributionWidget extends StatefulWidget {
 class MapAttributionWidget extends StatefulWidget {
@@ -87,6 +89,8 @@ class MapAttributionWidget extends StatefulWidget {
   ///
   ///
   /// Read the documentation on the individual properties for more information
   /// Read the documentation on the individual properties for more information
   /// and customizability.
   /// and customizability.
+
+  final double iconSize;
   const MapAttributionWidget({
   const MapAttributionWidget({
     super.key,
     super.key,
     required this.attributions,
     required this.attributions,
@@ -99,6 +103,7 @@ class MapAttributionWidget extends StatefulWidget {
     this.showFlutterMapAttribution = true,
     this.showFlutterMapAttribution = true,
     this.animationConfig = const FadeRAWA(),
     this.animationConfig = const FadeRAWA(),
     this.popupInitialDisplayDuration = Duration.zero,
     this.popupInitialDisplayDuration = Duration.zero,
+    this.iconSize = 20,
   });
   });
 
 
   @override
   @override
@@ -168,27 +173,23 @@ class MapAttributionWidgetState extends State<MapAttributionWidget> {
         duration: widget.animationConfig.buttonDuration,
         duration: widget.animationConfig.buttonDuration,
         child: popupExpanded
         child: popupExpanded
             ? (widget.closeButton ??
             ? (widget.closeButton ??
-                (context, close) => IconButton(
-                      onPressed: close,
-                      icon: Icon(
-                        Icons.cancel_outlined,
-                        color: Theme.of(context).textTheme.titleSmall?.color ??
-                            Colors.black,
-                        size: widget.permanentHeight,
-                      ),
+                (context, close) => IconButtonWidget(
+                      size: widget.iconSize,
+                      onTap: close,
+                      icon: Icons.cancel_outlined,
+                      iconButtonType: IconButtonType.primary,
+                      iconColor: getEnteColorScheme(context).strokeBase,
                     ))(
                     ))(
                 context,
                 context,
                 () => setState(() => popupExpanded = false),
                 () => setState(() => popupExpanded = false),
               )
               )
             : (widget.openButton ??
             : (widget.openButton ??
-                (context, open) => IconButton(
-                      onPressed: open,
-                      tooltip: 'Attributions',
-                      icon: Icon(
-                        Icons.info_outlined,
-                        size: widget.permanentHeight,
-                        color: getEnteColorScheme(context).backgroundElevated,
-                      ),
+                (context, open) => IconButtonWidget(
+                      size: widget.iconSize,
+                      onTap: open,
+                      icon: Icons.info_outlined,
+                      iconButtonType: IconButtonType.primary,
+                      iconColor: strokeBaseLight,
                     ))(
                     ))(
                 context,
                 context,
                 () {
                 () {

+ 25 - 4
lib/ui/map/tile/layers.dart

@@ -9,6 +9,18 @@ import "package:url_launcher/url_launcher_string.dart";
 
 
 const String _userAgent = "io.ente.photos";
 const String _userAgent = "io.ente.photos";
 
 
+class MapAttributionOptions {
+  final double permanentHeight;
+  final BorderRadius popupBorderRadius;
+  final double iconSize;
+
+  const MapAttributionOptions({
+    this.permanentHeight = 24,
+    this.popupBorderRadius = const BorderRadius.all(Radius.circular(12)),
+    this.iconSize = 20,
+  });
+}
+
 class OSMTileLayer extends StatelessWidget {
 class OSMTileLayer extends StatelessWidget {
   const OSMTileLayer({super.key});
   const OSMTileLayer({super.key});
 
 
@@ -42,28 +54,37 @@ class OSMFranceTileLayer extends StatelessWidget {
 }
 }
 
 
 class OSMFranceTileAttributes extends StatelessWidget {
 class OSMFranceTileAttributes extends StatelessWidget {
-  const OSMFranceTileAttributes({super.key});
+  final MapAttributionOptions options;
+  const OSMFranceTileAttributes({
+    this.options = const MapAttributionOptions(),
+    super.key,
+  });
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
+    final textTheme = getEnteTextTheme(context).tinyBold;
     return MapAttributionWidget(
     return MapAttributionWidget(
       alignment: AttributionAlignment.bottomLeft,
       alignment: AttributionAlignment.bottomLeft,
       showFlutterMapAttribution: false,
       showFlutterMapAttribution: false,
+      permanentHeight: options.permanentHeight,
+      popupBackgroundColor: getEnteColorScheme(context).backgroundElevated,
+      popupBorderRadius: options.popupBorderRadius,
+      iconSize: options.iconSize,
       attributions: [
       attributions: [
         TextSourceAttribution(
         TextSourceAttribution(
           S.of(context).openstreetmapContributors,
           S.of(context).openstreetmapContributors,
-          textStyle: getEnteTextTheme(context).smallBold,
+          textStyle: textTheme,
           onTap: () => launchUrlString('https://openstreetmap.org/copyright'),
           onTap: () => launchUrlString('https://openstreetmap.org/copyright'),
         ),
         ),
         TextSourceAttribution(
         TextSourceAttribution(
           'HOT Tiles',
           'HOT Tiles',
-          textStyle: getEnteTextTheme(context).smallBold,
+          textStyle: textTheme,
           onTap: () => launchUrl(Uri.parse('https://www.hotosm.org/')),
           onTap: () => launchUrl(Uri.parse('https://www.hotosm.org/')),
         ),
         ),
         TextSourceAttribution(
         TextSourceAttribution(
           S.of(context).hostedAtOsmFrance,
           S.of(context).hostedAtOsmFrance,
+          textStyle: textTheme,
           onTap: () => launchUrl(Uri.parse('https://www.openstreetmap.fr/')),
           onTap: () => launchUrl(Uri.parse('https://www.openstreetmap.fr/')),
-          textStyle: getEnteTextTheme(context).smallBold,
         ),
         ),
       ],
       ],
     );
     );

+ 31 - 8
lib/ui/notification/update/change_log_page.dart

@@ -1,3 +1,5 @@
+import "dart:async";
+
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import "package:photos/generated/l10n.dart";
 import "package:photos/generated/l10n.dart";
 import 'package:photos/services/update_service.dart';
 import 'package:photos/services/update_service.dart';
@@ -7,6 +9,7 @@ import 'package:photos/ui/components/divider_widget.dart';
 import 'package:photos/ui/components/models/button_type.dart';
 import 'package:photos/ui/components/models/button_type.dart';
 import 'package:photos/ui/components/title_bar_title_widget.dart';
 import 'package:photos/ui/components/title_bar_title_widget.dart';
 import 'package:photos/ui/notification/update/change_log_entry.dart';
 import 'package:photos/ui/notification/update/change_log_entry.dart';
+import "package:url_launcher/url_launcher_string.dart";
 
 
 class ChangeLogPage extends StatefulWidget {
 class ChangeLogPage extends StatefulWidget {
   const ChangeLogPage({
   const ChangeLogPage({
@@ -81,13 +84,28 @@ class _ChangeLogPageState extends State<ChangeLogPage> {
                     ButtonWidget(
                     ButtonWidget(
                       buttonType: ButtonType.trailingIconSecondary,
                       buttonType: ButtonType.trailingIconSecondary,
                       buttonSize: ButtonSize.large,
                       buttonSize: ButtonSize.large,
-                      labelText: S.of(context).rateTheApp,
-                      icon: Icons.favorite_rounded,
+                      labelText: S.of(context).joinDiscord,
+                      icon: Icons.discord_outlined,
                       iconColor: enteColorScheme.primary500,
                       iconColor: enteColorScheme.primary500,
                       onTap: () async {
                       onTap: () async {
-                        await UpdateService.instance.launchReviewUrl();
+                        unawaited(
+                          launchUrlString(
+                            "https://discord.com/invite/z2YVKkycX3",
+                            mode: LaunchMode.externalApplication,
+                          ),
+                        );
                       },
                       },
                     ),
                     ),
+                    // ButtonWidget(
+                    //   buttonType: ButtonType.trailingIconSecondary,
+                    //   buttonSize: ButtonSize.large,
+                    //   labelText: S.of(context).rateTheApp,
+                    //   icon: Icons.favorite_rounded,
+                    //   iconColor: enteColorScheme.primary500,
+                    //   onTap: () async {
+                    //     await UpdateService.instance.launchReviewUrl();
+                    //   },
+                    // ),
                     const SizedBox(height: 8),
                     const SizedBox(height: 8),
                   ],
                   ],
                 ),
                 ),
@@ -102,13 +120,18 @@ class _ChangeLogPageState extends State<ChangeLogPage> {
   Widget _getChangeLog() {
   Widget _getChangeLog() {
     final scrollController = ScrollController();
     final scrollController = ScrollController();
     final List<ChangeLogEntry> items = [];
     final List<ChangeLogEntry> items = [];
-    items.add(
+    items.addAll([
       ChangeLogEntry(
       ChangeLogEntry(
-        "Explore with the new Search Tab ✨",
-        'Introducing a dedicated search tab with distinct sections for effortless discovery.\n'
-            '\nYou can now discover items that come under different Locations, Moments, Contacts, Photo descriptions, Albums and File types with ease.\n',
+        "Map View ✨",
+        'You can now view the location where a photo was clicked.\n'
+            '\nOpen a photo and tap the Info button to view its place on the map!',
       ),
       ),
-    );
+      ChangeLogEntry(
+        "Bug Fixes",
+        'Many a bugs were squashed in this release.\n'
+            '\nIf you run into any, please write to team@ente.io, or let us know on Discord! 🙏',
+      ),
+    ]);
 
 
     return Container(
     return Container(
       padding: const EdgeInsets.only(left: 16),
       padding: const EdgeInsets.only(left: 16),

+ 1 - 0
lib/ui/viewer/file/file_details_widget.dart

@@ -145,6 +145,7 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
         },
         },
       ),
       ),
     );
     );
+
     fileDetailsTiles.addAll([
     fileDetailsTiles.addAll([
       ValueListenableBuilder(
       ValueListenableBuilder(
         valueListenable: hasLocationData,
         valueListenable: hasLocationData,

+ 8 - 0
lib/ui/viewer/file/video_widget_new.dart

@@ -7,6 +7,8 @@ import "package:logging/logging.dart";
 import "package:media_kit/media_kit.dart";
 import "package:media_kit/media_kit.dart";
 import "package:media_kit_video/media_kit_video.dart";
 import "package:media_kit_video/media_kit_video.dart";
 import "package:photos/core/constants.dart";
 import "package:photos/core/constants.dart";
+import "package:photos/core/event_bus.dart";
+import "package:photos/events/pause_video_event.dart";
 import "package:photos/generated/l10n.dart";
 import "package:photos/generated/l10n.dart";
 import "package:photos/models/file/extensions/file_props.dart";
 import "package:photos/models/file/extensions/file_props.dart";
 import "package:photos/models/file/file.dart";
 import "package:photos/models/file/file.dart";
@@ -43,6 +45,7 @@ class _VideoWidgetNewState extends State<VideoWidgetNew>
   final _progressNotifier = ValueNotifier<double?>(null);
   final _progressNotifier = ValueNotifier<double?>(null);
   late StreamSubscription<bool> playingStreamSubscription;
   late StreamSubscription<bool> playingStreamSubscription;
   bool _isAppInFG = true;
   bool _isAppInFG = true;
+  late StreamSubscription<PauseVideoEvent> pauseVideoSubscription;
 
 
   @override
   @override
   void initState() {
   void initState() {
@@ -83,6 +86,10 @@ class _VideoWidgetNewState extends State<VideoWidgetNew>
         widget.playbackCallback!(event);
         widget.playbackCallback!(event);
       }
       }
     });
     });
+
+    pauseVideoSubscription = Bus.instance.on<PauseVideoEvent>().listen((event) {
+      player.pause();
+    });
   }
   }
 
 
   @override
   @override
@@ -96,6 +103,7 @@ class _VideoWidgetNewState extends State<VideoWidgetNew>
 
 
   @override
   @override
   void dispose() {
   void dispose() {
+    pauseVideoSubscription.cancel();
     removeCallBack(widget.file);
     removeCallBack(widget.file);
     _progressNotifier.dispose();
     _progressNotifier.dispose();
     WidgetsBinding.instance.removeObserver(this);
     WidgetsBinding.instance.removeObserver(this);

+ 3 - 0
lib/ui/viewer/file_details/albums_item_widget.dart

@@ -1,6 +1,8 @@
 import "package:flutter/material.dart";
 import "package:flutter/material.dart";
 import "package:logging/logging.dart";
 import "package:logging/logging.dart";
+import "package:photos/core/event_bus.dart";
 import "package:photos/db/files_db.dart";
 import "package:photos/db/files_db.dart";
+import "package:photos/events/pause_video_event.dart";
 import "package:photos/generated/l10n.dart";
 import "package:photos/generated/l10n.dart";
 import 'package:photos/models/collection/collection.dart';
 import 'package:photos/models/collection/collection.dart';
 import 'package:photos/models/collection/collection_items.dart';
 import 'package:photos/models/collection/collection_items.dart';
@@ -87,6 +89,7 @@ class AlbumsItemWidget extends StatelessWidget {
               if (c.isHidden()) {
               if (c.isHidden()) {
                 return;
                 return;
               }
               }
+              Bus.instance.fire(PauseVideoEvent());
               routeToPage(
               routeToPage(
                 context,
                 context,
                 CollectionPage(
                 CollectionPage(

+ 2 - 2
lib/ui/viewer/file_details/file_properties_item_widget.dart

@@ -51,13 +51,13 @@ class _FilePropertiesItemWidgetState extends State<FilePropertiesItemWidget> {
     final StringBuffer dimString = StringBuffer();
     final StringBuffer dimString = StringBuffer();
     if (widget.exifData["resolution"] != null &&
     if (widget.exifData["resolution"] != null &&
         widget.exifData["megaPixels"] != null) {
         widget.exifData["megaPixels"] != null) {
-      dimString.write('${widget.exifData["megaPixels"]}MP ');
+      dimString.write('${widget.exifData["megaPixels"]}MP   ');
       dimString.write('${widget.exifData["resolution"]}');
       dimString.write('${widget.exifData["resolution"]}');
     } else if (widget.file.hasDimensions) {
     } else if (widget.file.hasDimensions) {
       final double megaPixels =
       final double megaPixels =
           (widget.file.width * widget.file.height) / 1000000;
           (widget.file.width * widget.file.height) / 1000000;
       final double roundedMegaPixels = (megaPixels * 10).round() / 10.0;
       final double roundedMegaPixels = (megaPixels * 10).round() / 10.0;
-      dimString.write('${roundedMegaPixels.toStringAsFixed(1)}MP ');
+      dimString.write('${roundedMegaPixels.toStringAsFixed(1)}MP   ');
       dimString.write('${widget.file.width} x ${widget.file.height}');
       dimString.write('${widget.file.width} x ${widget.file.height}');
     }
     }
     final subSectionWidgets = <Widget>[];
     final subSectionWidgets = <Widget>[];

+ 227 - 1
lib/ui/viewer/file_details/location_tags_widget.dart

@@ -1,15 +1,28 @@
 import "dart:async";
 import "dart:async";
+import "dart:ui";
 
 
 import "package:flutter/material.dart";
 import "package:flutter/material.dart";
+import "package:flutter_animate/flutter_animate.dart";
+import "package:flutter_map/flutter_map.dart";
+import "package:latlong2/latlong.dart";
+
 import "package:photos/core/event_bus.dart";
 import "package:photos/core/event_bus.dart";
 import "package:photos/events/location_tag_updated_event.dart";
 import "package:photos/events/location_tag_updated_event.dart";
 import "package:photos/generated/l10n.dart";
 import "package:photos/generated/l10n.dart";
 import "package:photos/models/file/file.dart";
 import "package:photos/models/file/file.dart";
 import "package:photos/services/location_service.dart";
 import "package:photos/services/location_service.dart";
+import "package:photos/services/search_service.dart";
+import "package:photos/services/user_remote_flag_service.dart";
 import "package:photos/states/location_screen_state.dart";
 import "package:photos/states/location_screen_state.dart";
 import "package:photos/theme/ente_theme.dart";
 import "package:photos/theme/ente_theme.dart";
 import "package:photos/ui/components/buttons/chip_button_widget.dart";
 import "package:photos/ui/components/buttons/chip_button_widget.dart";
 import "package:photos/ui/components/info_item_widget.dart";
 import "package:photos/ui/components/info_item_widget.dart";
+import "package:photos/ui/map/enable_map.dart";
+import "package:photos/ui/map/image_marker.dart";
+import "package:photos/ui/map/map_screen.dart";
+import "package:photos/ui/map/map_view.dart";
+import "package:photos/ui/map/tile/layers.dart";
+
 import 'package:photos/ui/viewer/location/add_location_sheet.dart';
 import 'package:photos/ui/viewer/location/add_location_sheet.dart';
 import "package:photos/ui/viewer/location/location_screen.dart";
 import "package:photos/ui/viewer/location/location_screen.dart";
 import "package:photos/utils/navigation_util.dart";
 import "package:photos/utils/navigation_util.dart";
@@ -29,13 +42,19 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
   late Future<List<Widget>> locationTagChips;
   late Future<List<Widget>> locationTagChips;
   late StreamSubscription<LocationTagUpdatedEvent> _locTagUpdateListener;
   late StreamSubscription<LocationTagUpdatedEvent> _locTagUpdateListener;
   VoidCallback? onTap;
   VoidCallback? onTap;
+  bool _loadedLocationTags = false;
+
   @override
   @override
   void initState() {
   void initState() {
-    locationTagChips = _getLocationTags();
+    locationTagChips = _getLocationTags().then((value) {
+      _loadedLocationTags = true;
+      return value;
+    });
     _locTagUpdateListener =
     _locTagUpdateListener =
         Bus.instance.on<LocationTagUpdatedEvent>().listen((event) {
         Bus.instance.on<LocationTagUpdatedEvent>().listen((event) {
       locationTagChips = _getLocationTags();
       locationTagChips = _getLocationTags();
     });
     });
+
     super.initState();
     super.initState();
   }
   }
 
 
@@ -58,6 +77,9 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
         subtitleSection: locationTagChips,
         subtitleSection: locationTagChips,
         hasChipButtons: hasChipButtons ?? true,
         hasChipButtons: hasChipButtons ?? true,
         onTap: onTap,
         onTap: onTap,
+        endSection: _loadedLocationTags
+            ? InfoMap(widget.file)
+            : const SizedBox.shrink(),
 
 
         /// to be used when state issues are fixed when location is updated
         /// to be used when state issues are fixed when location is updated
         // editOnTap: widget.file.ownerID == Configuration.instance.getUserID()!
         // editOnTap: widget.file.ownerID == Configuration.instance.getUserID()!
@@ -83,6 +105,7 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
   }
   }
 
 
   Future<List<Widget>> _getLocationTags() async {
   Future<List<Widget>> _getLocationTags() async {
+    // await Future.delayed(const Duration(seconds: 1));
     final locationTags = await LocationService.instance
     final locationTags = await LocationService.instance
         .enclosingLocationTags(widget.file.location!);
         .enclosingLocationTags(widget.file.location!);
     if (locationTags.isEmpty) {
     if (locationTags.isEmpty) {
@@ -139,3 +162,206 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
     }
     }
   }
   }
 }
 }
+
+class InfoMap extends StatefulWidget {
+  final EnteFile file;
+  const InfoMap(this.file, {super.key});
+
+  @override
+  State<InfoMap> createState() => _InfoMapState();
+}
+
+class _InfoMapState extends State<InfoMap> {
+  final _mapController = MapController();
+  late bool _hasEnabledMap;
+  late double _fileLat;
+  late double _fileLng;
+  static const _enabledMapZoom = 12.0;
+  static const _disabledMapZoom = 9.0;
+  bool _tappedToOpenMap = false;
+  final _past250msAfterInit = ValueNotifier(false);
+
+  @override
+  void initState() {
+    super.initState();
+    _hasEnabledMap = UserRemoteFlagService.instance
+        .getCachedBoolValue(UserRemoteFlagService.mapEnabled);
+    _fileLat = widget.file.location!.latitude!;
+    _fileLng = widget.file.location!.longitude!;
+
+    Future.delayed(const Duration(milliseconds: 250), () {
+      _past250msAfterInit.value = true;
+    });
+  }
+
+  @override
+  void dispose() {
+    _mapController.dispose();
+    _past250msAfterInit.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.only(top: 8),
+      child: ClipRRect(
+        clipBehavior: Clip.antiAliasWithSaveLayer,
+        borderRadius: const BorderRadius.all(Radius.circular(8)),
+        child: SizedBox(
+          height: 124,
+          child: _hasEnabledMap
+              ? Stack(
+                  clipBehavior: Clip.none,
+                  key: ValueKey(_hasEnabledMap),
+                  children: [
+                    MapView(
+                      updateVisibleImages: () {},
+                      imageMarkers: [
+                        ImageMarker(
+                          imageFile: widget.file,
+                          latitude: _fileLat,
+                          longitude: _fileLng,
+                        ),
+                      ],
+                      controller: _mapController,
+                      center: LatLng(
+                        _fileLat,
+                        _fileLng,
+                      ),
+                      minZoom: _enabledMapZoom,
+                      maxZoom: _enabledMapZoom,
+                      initialZoom: _enabledMapZoom,
+                      bottomSheetDraggableAreaHeight: 0,
+                      showControls: false,
+                      interactiveFlags: InteractiveFlag.none,
+                      mapAttributionOptions: MapAttributionOptions(
+                        permanentHeight: 16,
+                        popupBorderRadius: BorderRadius.circular(4),
+                        iconSize: 16,
+                      ),
+                      onTap: enabledMapOnTap,
+                      markerSize: const Size(45, 45),
+                    ),
+                    IgnorePointer(
+                      child: Container(
+                        decoration: BoxDecoration(
+                          borderRadius: BorderRadius.circular(8),
+                          border: Border.all(
+                            color: getEnteColorScheme(context).strokeFaint,
+                          ),
+                        ),
+                      ),
+                    ),
+                  ],
+                )
+              : ValueListenableBuilder(
+                  valueListenable: _past250msAfterInit,
+                  builder: (context, value, _) {
+                    return value
+                        ? Stack(
+                            key: ValueKey(_hasEnabledMap),
+                            clipBehavior: Clip.none,
+                            children: [
+                              MapView(
+                                updateVisibleImages: () {},
+                                imageMarkers: const [],
+                                controller: _mapController,
+                                center: const LatLng(
+                                  13.041599,
+                                  77.594566,
+                                ),
+                                minZoom: _disabledMapZoom,
+                                maxZoom: _disabledMapZoom,
+                                initialZoom: _disabledMapZoom,
+                                bottomSheetDraggableAreaHeight: 0,
+                                showControls: false,
+                                interactiveFlags: InteractiveFlag.none,
+                                mapAttributionOptions:
+                                    const MapAttributionOptions(
+                                  iconSize: 0,
+                                ),
+                              ),
+                              BackdropFilter(
+                                filter: ImageFilter.blur(
+                                  sigmaX: 2.8,
+                                  sigmaY: 2.8,
+                                ),
+                                child: Container(
+                                  color: getEnteColorScheme(context)
+                                      .backgroundElevated
+                                      .withOpacity(0.5),
+                                ),
+                              ),
+                              Container(
+                                decoration: BoxDecoration(
+                                  borderRadius: BorderRadius.circular(8),
+                                  border: Border.all(
+                                    color:
+                                        getEnteColorScheme(context).strokeFaint,
+                                  ),
+                                ),
+                              ),
+                              GestureDetector(
+                                behavior: HitTestBehavior.opaque,
+                                onTap: () async {
+                                  unawaited(
+                                    requestForMapEnable(context).then((value) {
+                                      if (value) {
+                                        setState(() {
+                                          _hasEnabledMap = true;
+                                        });
+                                      }
+                                    }),
+                                  );
+                                },
+                                child: Center(
+                                  child: Text(
+                                    S.of(context).enableMaps,
+                                    style: getEnteTextTheme(context).small,
+                                  ),
+                                ),
+                              ),
+                            ],
+                          ).animate().fadeIn(
+                              duration: const Duration(milliseconds: 90),
+                              curve: Curves.easeIn,
+                            )
+                        : const SizedBox.shrink();
+                  },
+                ),
+        ),
+      ),
+    ).animate(target: _tappedToOpenMap ? 1 : 0).scaleXY(
+          end: 1.025,
+          duration: const Duration(milliseconds: 220),
+          curve: Curves.easeInOut,
+        );
+  }
+
+  void enabledMapOnTap() async {
+    setState(() {
+      _tappedToOpenMap = true;
+    });
+    unawaited(
+      Navigator.of(context)
+          .push(
+        MaterialPageRoute(
+          builder: (context) => MapScreen(
+            filesFutureFn: SearchService.instance.getAllFiles,
+            center: LatLng(
+              _fileLat,
+              _fileLng,
+            ),
+            initialZoom: 16,
+          ),
+        ),
+      )
+          .then((value) {
+        setState(() {
+          _tappedToOpenMap = false;
+        });
+      }),
+    );
+  }
+}

+ 14 - 19
lib/ui/viewer/gallery/component/group/lazy_group_gallery.dart

@@ -56,8 +56,7 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
 
 
   late Logger _logger;
   late Logger _logger;
 
 
-  late List<EnteFile> _files;
-  Set<EnteFile>? _filesAsSet;
+  late List<EnteFile> _filesInGroup;
   late StreamSubscription<FilesUpdatedEvent>? _reloadEventSubscription;
   late StreamSubscription<FilesUpdatedEvent>? _reloadEventSubscription;
   late StreamSubscription<int> _currentIndexSubscription;
   late StreamSubscription<int> _currentIndexSubscription;
   bool? _shouldRender;
   bool? _shouldRender;
@@ -65,7 +64,8 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
   @override
   @override
   void initState() {
   void initState() {
     super.initState();
     super.initState();
-    _areAllFromGroupSelectedNotifier = ValueNotifier(_areAllFromGroupSelected());
+    _areAllFromGroupSelectedNotifier =
+        ValueNotifier(_areAllFromGroupSelected());
 
 
     widget.selectedFiles?.addListener(_selectedFilesListener);
     widget.selectedFiles?.addListener(_selectedFilesListener);
     _showSelectAllButtonNotifier = ValueNotifier(widget.showSelectAllByDefault);
     _showSelectAllButtonNotifier = ValueNotifier(widget.showSelectAllByDefault);
@@ -75,7 +75,7 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
   void _init() {
   void _init() {
     _logger = Logger("LazyLoading_${widget.logTag}");
     _logger = Logger("LazyLoading_${widget.logTag}");
     _shouldRender = true;
     _shouldRender = true;
-    _files = widget.files;
+    _filesInGroup = widget.files;
     _areAllFromGroupSelectedNotifier.value = _areAllFromGroupSelected();
     _areAllFromGroupSelectedNotifier.value = _areAllFromGroupSelected();
     _reloadEventSubscription = widget.reloadEvent?.listen((e) => _onReload(e));
     _reloadEventSubscription = widget.reloadEvent?.listen((e) => _onReload(e));
 
 
@@ -91,11 +91,6 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
     });
     });
   }
   }
 
 
-  Set<EnteFile> get _setOfFiles {
-    _filesAsSet ??= _files.toSet();
-    return _filesAsSet!;
-  }
-
   bool _areAllFromGroupSelected() {
   bool _areAllFromGroupSelected() {
     if (widget.selectedFiles != null &&
     if (widget.selectedFiles != null &&
         widget.selectedFiles!.files.length >= widget.files.length) {
         widget.selectedFiles!.files.length >= widget.files.length) {
@@ -106,11 +101,11 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
   }
   }
 
 
   Future _onReload(FilesUpdatedEvent event) async {
   Future _onReload(FilesUpdatedEvent event) async {
-    if (_files.isEmpty) {
+    if (_filesInGroup.isEmpty) {
       return;
       return;
     }
     }
     final DateTime groupDate =
     final DateTime groupDate =
-        DateTime.fromMicrosecondsSinceEpoch(_files[0].creationTime!);
+        DateTime.fromMicrosecondsSinceEpoch(_filesInGroup[0].creationTime!);
     // iterate over  files and check if any of the belongs to this group
     // iterate over  files and check if any of the belongs to this group
     final anyCandidateForGroup = event.updatedFiles.any((file) {
     final anyCandidateForGroup = event.updatedFiles.any((file) {
       final fileDate = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
       final fileDate = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
@@ -152,7 +147,7 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
         final galleryState = context.findAncestorStateOfType<GalleryState>();
         final galleryState = context.findAncestorStateOfType<GalleryState>();
         if (galleryState?.mounted ?? false) {
         if (galleryState?.mounted ?? false) {
           galleryState!.setState(() {});
           galleryState!.setState(() {});
-          _files = result.files;
+          _filesInGroup = result.files;
         }
         }
       } else if (kDebugMode) {
       } else if (kDebugMode) {
         debugPrint("Unexpected event ${event.type.name}");
         debugPrint("Unexpected event ${event.type.name}");
@@ -172,7 +167,7 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
   @override
   @override
   void didUpdateWidget(LazyGroupGallery oldWidget) {
   void didUpdateWidget(LazyGroupGallery oldWidget) {
     super.didUpdateWidget(oldWidget);
     super.didUpdateWidget(oldWidget);
-    if (!listEquals(_files, widget.files)) {
+    if (!listEquals(_filesInGroup, widget.files)) {
       _reloadEventSubscription?.cancel();
       _reloadEventSubscription?.cancel();
       _init();
       _init();
     }
     }
@@ -180,7 +175,7 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
-    if (_files.isEmpty) {
+    if (_filesInGroup.isEmpty) {
       return const SizedBox.shrink();
       return const SizedBox.shrink();
     }
     }
     return Column(
     return Column(
@@ -190,7 +185,7 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
           children: [
           children: [
             if (widget.enableFileGrouping)
             if (widget.enableFileGrouping)
               GroupHeaderWidget(
               GroupHeaderWidget(
-                timestamp: _files[0].creationTime!,
+                timestamp: _filesInGroup[0].creationTime!,
                 gridSize: widget.photoGridSize,
                 gridSize: widget.photoGridSize,
               ),
               ),
             Expanded(child: Container()),
             Expanded(child: Container()),
@@ -226,7 +221,7 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
                               ),
                               ),
                               onTap: () {
                               onTap: () {
                                 widget.selectedFiles?.toggleGroupSelection(
                                 widget.selectedFiles?.toggleGroupSelection(
-                                  _setOfFiles,
+                                  _filesInGroup.toSet(),
                                 );
                                 );
                               },
                               },
                             );
                             );
@@ -237,7 +232,7 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
         _shouldRender!
         _shouldRender!
             ? GroupGallery(
             ? GroupGallery(
                 photoGridSize: widget.photoGridSize,
                 photoGridSize: widget.photoGridSize,
-                files: _files,
+                files: _filesInGroup,
                 tag: widget.tag,
                 tag: widget.tag,
                 asyncLoader: widget.asyncLoader,
                 asyncLoader: widget.asyncLoader,
                 selectedFiles: widget.selectedFiles,
                 selectedFiles: widget.selectedFiles,
@@ -246,7 +241,7 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
             // todo: perf eval should we have separate PlaceHolder for Groups
             // todo: perf eval should we have separate PlaceHolder for Groups
             //  instead of creating a large cached view
             //  instead of creating a large cached view
             : PlaceHolderGridViewWidget(
             : PlaceHolderGridViewWidget(
-                _files.length,
+                _filesInGroup.length,
                 widget.photoGridSize,
                 widget.photoGridSize,
               ),
               ),
       ],
       ],
@@ -256,7 +251,7 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
   void _selectedFilesListener() {
   void _selectedFilesListener() {
     if (widget.selectedFiles == null) return;
     if (widget.selectedFiles == null) return;
     _areAllFromGroupSelectedNotifier.value =
     _areAllFromGroupSelectedNotifier.value =
-        widget.selectedFiles!.files.containsAll(_setOfFiles);
+        widget.selectedFiles!.files.containsAll(_filesInGroup.toSet());
 
 
     //Can remove this if we decide to show select all by default for all galleries
     //Can remove this if we decide to show select all by default for all galleries
     if (widget.selectedFiles!.files.isEmpty && !widget.showSelectAllByDefault) {
     if (widget.selectedFiles!.files.isEmpty && !widget.showSelectAllByDefault) {

+ 60 - 0
lib/ui/viewer/gallery/gallery_app_bar_widget.dart

@@ -6,10 +6,14 @@ import "package:flutter/cupertino.dart";
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/configuration.dart';
+import "package:photos/core/constants.dart";
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/event_bus.dart';
+import "package:photos/core/network/network.dart";
 import "package:photos/db/files_db.dart";
 import "package:photos/db/files_db.dart";
 import 'package:photos/events/subscription_purchased_event.dart';
 import 'package:photos/events/subscription_purchased_event.dart';
+import "package:photos/gateways/cast_gw.dart";
 import "package:photos/generated/l10n.dart";
 import "package:photos/generated/l10n.dart";
+import "package:photos/l10n/l10n.dart";
 import 'package:photos/models/backup_status.dart';
 import 'package:photos/models/backup_status.dart';
 import 'package:photos/models/collection/collection.dart';
 import 'package:photos/models/collection/collection.dart';
 import 'package:photos/models/device_collection.dart';
 import 'package:photos/models/device_collection.dart';
@@ -36,6 +40,7 @@ import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/magic_util.dart';
 import 'package:photos/utils/magic_util.dart';
 import 'package:photos/utils/navigation_util.dart';
 import 'package:photos/utils/navigation_util.dart';
 import 'package:photos/utils/toast_util.dart';
 import 'package:photos/utils/toast_util.dart';
+import "package:uuid/uuid.dart";
 
 
 class GalleryAppBarWidget extends StatefulWidget {
 class GalleryAppBarWidget extends StatefulWidget {
   final GalleryType type;
   final GalleryType type;
@@ -64,6 +69,7 @@ enum AlbumPopupAction {
   ownedArchive,
   ownedArchive,
   sharedArchive,
   sharedArchive,
   ownedHide,
   ownedHide,
+  playOnTv,
   sort,
   sort,
   leave,
   leave,
   freeUpSpace,
   freeUpSpace,
@@ -472,6 +478,22 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
         ),
         ),
       );
       );
     }
     }
+    if (widget.collection != null && isInternalUser) {
+      items.add(
+        PopupMenuItem(
+          value: AlbumPopupAction.playOnTv,
+          child: Row(
+            children: [
+              const Icon(Icons.tv_outlined),
+              const Padding(
+                padding: EdgeInsets.all(8),
+              ),
+              Text(context.l10n.playOnTv),
+            ],
+          ),
+        ),
+      );
+    }
 
 
     if (galleryType.canDelete()) {
     if (galleryType.canDelete()) {
       items.add(
       items.add(
@@ -579,6 +601,8 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
               await _removeQuickLink();
               await _removeQuickLink();
             } else if (value == AlbumPopupAction.leave) {
             } else if (value == AlbumPopupAction.leave) {
               await _leaveAlbum(context);
               await _leaveAlbum(context);
+            } else if (value == AlbumPopupAction.playOnTv) {
+              await castAlbum();
             } else if (value == AlbumPopupAction.freeUpSpace) {
             } else if (value == AlbumPopupAction.freeUpSpace) {
               await _deleteBackedUpFiles(context);
               await _deleteBackedUpFiles(context);
             } else if (value == AlbumPopupAction.setCover) {
             } else if (value == AlbumPopupAction.setCover) {
@@ -797,4 +821,40 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
     );
     );
     setState(() {});
     setState(() {});
   }
   }
+
+  Future<void> castAlbum() async {
+    final gw = CastGateway(NetworkClient.instance.enteDio);
+    // stop any existing cast session
+    gw.revokeAllTokens().ignore();
+    await showTextInputDialog(
+      context,
+      title: context.l10n.playOnTv,
+      body: S.of(context).castInstruction,
+      submitButtonLabel: S.of(context).pair,
+      textInputType: TextInputType.streetAddress,
+      hintText: context.l10n.deviceCodeHint,
+      onSubmit: (String text) async {
+        try {
+          String code = text.trim();
+          final String? publicKey = await gw.getPublicKey(code);
+          if (publicKey == null) {
+            showToast(context, S.of(context).deviceNotFound);
+            return;
+          }
+          final String castToken = Uuid().v4().toString();
+          final castPayload = CollectionsService.instance
+              .getCastData(castToken, widget.collection!, publicKey);
+          await gw.publishCastPayload(
+            code,
+            castPayload,
+            widget.collection!.id,
+            castToken,
+          );
+        } catch (e, s) {
+          _logger.severe("Failed to cast album", e, s);
+          await showGenericErrorDialog(context: context, error: e);
+        }
+      },
+    );
+  }
 }
 }

+ 39 - 1
lib/utils/exif_util.dart

@@ -11,6 +11,11 @@ import 'package:photos/utils/file_util.dart';
 
 
 const kDateTimeOriginal = "EXIF DateTimeOriginal";
 const kDateTimeOriginal = "EXIF DateTimeOriginal";
 const kImageDateTime = "Image DateTime";
 const kImageDateTime = "Image DateTime";
+const kExifOffSetKeys = [
+  "EXIF OffsetTime",
+  "EXIF OffsetTimeOriginal",
+  "EXIF OffsetTimeDigitized",
+];
 const kExifDateTimePattern = "yyyy:MM:dd HH:mm:ss";
 const kExifDateTimePattern = "yyyy:MM:dd HH:mm:ss";
 const kEmptyExifDateTime = "0000:00:00 00:00:00";
 const kEmptyExifDateTime = "0000:00:00 00:00:00";
 
 
@@ -56,7 +61,14 @@ Future<DateTime?> getCreationTimeFromEXIF(
             ? exif[kImageDateTime]!.printable
             ? exif[kImageDateTime]!.printable
             : null;
             : null;
     if (exifTime != null && exifTime != kEmptyExifDateTime) {
     if (exifTime != null && exifTime != kEmptyExifDateTime) {
-      return DateFormat(kExifDateTimePattern).parse(exifTime);
+      String? exifOffsetTime;
+      for (final key in kExifOffSetKeys) {
+        if (exif.containsKey(key)) {
+          exifOffsetTime = exif[key]!.printable;
+          break;
+        }
+      }
+      return getDateTimeInDeviceTimezone(exifTime, exifOffsetTime);
     }
     }
   } catch (e) {
   } catch (e) {
     _logger.severe("failed to getCreationTimeFromEXIF", e);
     _logger.severe("failed to getCreationTimeFromEXIF", e);
@@ -64,6 +76,32 @@ Future<DateTime?> getCreationTimeFromEXIF(
   return null;
   return null;
 }
 }
 
 
+DateTime getDateTimeInDeviceTimezone(String exifTime, String? offsetString) {
+  final DateTime result = DateFormat(kExifDateTimePattern).parse(exifTime);
+  if (offsetString == null) {
+    return result;
+  }
+  try {
+    final List<String> splitHHMM = offsetString.split(":");
+    // Parse the offset from the photo's time zone
+    final int offsetHours = int.parse(splitHHMM[0]);
+    final int offsetMinutes =
+        int.parse(splitHHMM[1]) * (offsetHours.isNegative ? -1 : 1);
+    // Adjust the date for the offset to get the photo's correct UTC time
+    final photoUtcDate =
+        result.add(Duration(hours: -offsetHours, minutes: -offsetMinutes));
+    // Getting the current device's time zone offset from UTC
+    final now = DateTime.now();
+    final localOffset = now.timeZoneOffset;
+    // Adjusting the photo's UTC time to the device's local time
+    final deviceLocalTime = photoUtcDate.add(localOffset);
+    return deviceLocalTime;
+  } catch (e, s) {
+    _logger.severe("tz offset adjust failed $offsetString", e, s);
+  }
+  return result;
+}
+
 Location? locationFromExif(Map<String, IfdTag> exif) {
 Location? locationFromExif(Map<String, IfdTag> exif) {
   try {
   try {
     return gpsDataFromExif(exif).toLocationObj();
     return gpsDataFromExif(exif).toLocationObj();

+ 1 - 1
pubspec.yaml

@@ -12,7 +12,7 @@ description: ente photos application
 # Read more about iOS versioning at
 # Read more about iOS versioning at
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 
 
-version: 0.8.49+569
+version: 0.8.55+575
 publish_to: none
 publish_to: none
 
 
 environment:
 environment: