diff --git a/CHANGELOG.md b/CHANGELOG.md index 16733e963..947f855e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # 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 diff --git a/lib/events/pause_video_event.dart b/lib/events/pause_video_event.dart new file mode 100644 index 000000000..3583abddb --- /dev/null +++ b/lib/events/pause_video_event.dart @@ -0,0 +1,3 @@ +import "package:photos/events/event.dart"; + +class PauseVideoEvent extends Event {} diff --git a/lib/gateways/cast_gw.dart b/lib/gateways/cast_gw.dart new file mode 100644 index 000000000..fb342c1a9 --- /dev/null +++ b/lib/gateways/cast_gw.dart @@ -0,0 +1,50 @@ +import "package:dio/dio.dart"; + +class CastGateway { + final Dio _enteDio; + + CastGateway(this._enteDio); + + Future 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 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 revokeAllTokens() async { + try { + await _enteDio.delete( + "/cast/revoke-all-tokens/", + ); + } catch (e) { + // swallow error + } + } +} diff --git a/lib/generated/intl/messages_cs.dart b/lib/generated/intl/messages_cs.dart index e91d3502e..a824ca356 100644 --- a/lib/generated/intl/messages_cs.dart +++ b/lib/generated/intl/messages_cs.dart @@ -34,6 +34,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Edits to location will only be seen within Ente"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), + "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "modifyYourQueryOrTrySearchingFor": MessageLookupByLibrary.simpleMessage( "Modify your query, or try searching for"), diff --git a/lib/generated/intl/messages_de.dart b/lib/generated/intl/messages_de.dart index 7d208e1ec..2c766c0a9 100644 --- a/lib/generated/intl/messages_de.dart +++ b/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"), "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Ausgewählte Elemente werden aus diesem Album entfernt"), + "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "keepPhotos": MessageLookupByLibrary.simpleMessage("Fotos behalten"), "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"), "kindlyHelpUsWithThisInformation": @@ -845,7 +846,7 @@ class MessageLookup extends MessageLookupByLibrary { "loadMessage1": MessageLookupByLibrary.simpleMessage( "Du kannst dein Abonnement mit deiner Familie teilen"), "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( "Wir behalten 3 Kopien Ihrer Daten, eine in einem unterirdischen Schutzbunker"), "loadMessage4": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 254eacaa9..7675ae171 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -380,6 +380,8 @@ class MessageLookup extends MessageLookupByLibrary { "cannotAddMorePhotosAfterBecomingViewer": m7, "cannotDeleteSharedFiles": 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"), "changeEmail": MessageLookupByLibrary.simpleMessage("Change email"), "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( @@ -552,10 +554,14 @@ class MessageLookup extends MessageLookupByLibrary { "details": MessageLookupByLibrary.simpleMessage("Details"), "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."), + "deviceCodeHint": + MessageLookupByLibrary.simpleMessage("Enter the code"), "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage( "Files added to this device album will automatically get uploaded to ente."), "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."), + "deviceNotFound": + MessageLookupByLibrary.simpleMessage("Device not found"), "didYouKnow": MessageLookupByLibrary.simpleMessage("Did you know?"), "disableAutoLock": MessageLookupByLibrary.simpleMessage("Disable auto lock"), @@ -784,6 +790,7 @@ class MessageLookup extends MessageLookupByLibrary { "Items show the number of days remaining before permanent deletion"), "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Selected items will be removed from this album"), + "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "keepPhotos": MessageLookupByLibrary.simpleMessage("Keep Photos"), "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"), "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( @@ -811,7 +818,7 @@ class MessageLookup extends MessageLookupByLibrary { "loadMessage1": MessageLookupByLibrary.simpleMessage( "You can share your subscription with your family"), "loadMessage2": MessageLookupByLibrary.simpleMessage( - "We have preserved over 10 million memories so far"), + "We have preserved over 30 million memories so far"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "We keep 3 copies of your data, one in an underground fallout shelter"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -946,6 +953,7 @@ class MessageLookup extends MessageLookupByLibrary { "Optional, as short as you like..."), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Or pick an existing one"), + "pair": MessageLookupByLibrary.simpleMessage("Pair"), "password": MessageLookupByLibrary.simpleMessage("Password"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Password changed successfully"), @@ -980,6 +988,7 @@ class MessageLookup extends MessageLookupByLibrary { "pickCenterPoint": MessageLookupByLibrary.simpleMessage("Pick center point"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Pin album"), + "playOnTv": MessageLookupByLibrary.simpleMessage("Play album on TV"), "playStoreFreeTrialValidTill": m37, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore subscription"), diff --git a/lib/generated/intl/messages_es.dart b/lib/generated/intl/messages_es.dart index 95868cb62..a33d6e88a 100644 --- a/lib/generated/intl/messages_es.dart +++ b/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"), "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Los elementos seleccionados serán removidos de este álbum"), + "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "keepPhotos": MessageLookupByLibrary.simpleMessage("Conservar las fotos"), "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"), @@ -733,7 +734,7 @@ class MessageLookup extends MessageLookupByLibrary { "loadMessage1": MessageLookupByLibrary.simpleMessage( "Puedes compartir tu suscripción con tu familia"), "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( "Guardamos 3 copias de sus datos, una en un refugio subterráneo"), "loadMessage4": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_fr.dart b/lib/generated/intl/messages_fr.dart index abeb3c550..39287c79a 100644 --- a/lib/generated/intl/messages_fr.dart +++ b/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"), "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Les éléments sélectionnés seront supprimés de cet album"), + "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "keepPhotos": MessageLookupByLibrary.simpleMessage("Conserver les photos"), "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"), @@ -843,7 +844,7 @@ class MessageLookup extends MessageLookupByLibrary { "loadMessage1": MessageLookupByLibrary.simpleMessage( "Vous pouvez partager votre abonnement avec votre famille"), "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( "Nous conservons 3 copies de vos données, l\'une dans un abri anti-atomique"), "loadMessage4": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_it.dart b/lib/generated/intl/messages_it.dart index 7e4ef27f4..29367dd33 100644 --- a/lib/generated/intl/messages_it.dart +++ b/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"), "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Gli elementi selezionati saranno rimossi da questo album"), + "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "keepPhotos": MessageLookupByLibrary.simpleMessage("Mantieni foto"), "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"), "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( @@ -810,7 +811,7 @@ class MessageLookup extends MessageLookupByLibrary { "loadMessage1": MessageLookupByLibrary.simpleMessage( "Puoi condividere il tuo abbonamento con la tua famiglia"), "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( "Teniamo 3 copie dei tuoi dati, uno in un rifugio sotterraneo antiatomico"), "loadMessage4": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_ko.dart b/lib/generated/intl/messages_ko.dart index d9c3fcd9e..903b11f1a 100644 --- a/lib/generated/intl/messages_ko.dart +++ b/lib/generated/intl/messages_ko.dart @@ -34,6 +34,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Edits to location will only be seen within Ente"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), + "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "modifyYourQueryOrTrySearchingFor": MessageLookupByLibrary.simpleMessage( "Modify your query, or try searching for"), diff --git a/lib/generated/intl/messages_nl.dart b/lib/generated/intl/messages_nl.dart index 71cd18855..6b2b97ff8 100644 --- a/lib/generated/intl/messages_nl.dart +++ b/lib/generated/intl/messages_nl.dart @@ -392,6 +392,8 @@ class MessageLookup extends MessageLookupByLibrary { "cannotAddMorePhotosAfterBecomingViewer": m7, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( "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"), "changeEmail": MessageLookupByLibrary.simpleMessage("E-mail wijzigen"), "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( @@ -569,10 +571,14 @@ class MessageLookup extends MessageLookupByLibrary { "details": MessageLookupByLibrary.simpleMessage("Details"), "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."), + "deviceCodeHint": + MessageLookupByLibrary.simpleMessage("Voer de code in"), "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage( "Bestanden toegevoegd aan dit album van dit apparaat zullen automatisch geüpload worden naar ente."), "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."), + "deviceNotFound": + MessageLookupByLibrary.simpleMessage("Apparaat niet gevonden"), "didYouKnow": MessageLookupByLibrary.simpleMessage("Wist u dat?"), "disableAutoLock": MessageLookupByLibrary.simpleMessage( "Automatisch vergrendelen uitschakelen"), @@ -816,6 +822,7 @@ class MessageLookup extends MessageLookupByLibrary { "Bestanden tonen het aantal resterende dagen voordat ze permanent worden verwijderd"), "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Geselecteerde items zullen worden verwijderd uit dit album"), + "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "keepPhotos": MessageLookupByLibrary.simpleMessage("Foto\'s behouden"), "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"), "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( @@ -987,6 +994,7 @@ class MessageLookup extends MessageLookupByLibrary { "Optioneel, zo kort als je wilt..."), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Of kies een bestaande"), + "pair": MessageLookupByLibrary.simpleMessage("Koppelen"), "password": MessageLookupByLibrary.simpleMessage("Wachtwoord"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Wachtwoord succesvol aangepast"), @@ -1025,6 +1033,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Kies middelpunt"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Album bovenaan vastzetten"), + "playOnTv": + MessageLookupByLibrary.simpleMessage("Album afspelen op TV"), "playStoreFreeTrialValidTill": m37, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore abonnement"), diff --git a/lib/generated/intl/messages_no.dart b/lib/generated/intl/messages_no.dart index 97bb1d5ea..f1e3324f3 100644 --- a/lib/generated/intl/messages_no.dart +++ b/lib/generated/intl/messages_no.dart @@ -54,6 +54,7 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "invalidEmailAddress": MessageLookupByLibrary.simpleMessage("Ugyldig e-postadresse"), + "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Vær vennlig og hjelp oss med denne informasjonen"), "modifyYourQueryOrTrySearchingFor": diff --git a/lib/generated/intl/messages_pl.dart b/lib/generated/intl/messages_pl.dart index aecb7c89b..debe5b6a8 100644 --- a/lib/generated/intl/messages_pl.dart +++ b/lib/generated/intl/messages_pl.dart @@ -113,6 +113,7 @@ class MessageLookup extends MessageLookupByLibrary { "Nieprawidłowy klucz odzyskiwania"), "invalidEmailAddress": MessageLookupByLibrary.simpleMessage("Nieprawidłowy adres e-mail"), + "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage("Pomóż nam z tą informacją"), "logInLabel": MessageLookupByLibrary.simpleMessage("Zaloguj się"), diff --git a/lib/generated/intl/messages_pt.dart b/lib/generated/intl/messages_pt.dart index 28bf3ac07..fb75fba87 100644 --- a/lib/generated/intl/messages_pt.dart +++ b/lib/generated/intl/messages_pt.dart @@ -252,6 +252,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Convide seus amigos"), "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Os itens selecionados serão removidos deste álbum"), + "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "keepPhotos": MessageLookupByLibrary.simpleMessage("Manter fotos"), "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Ajude-nos com esta informação"), diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart index d8e1f1f44..234b74bbc 100644 --- a/lib/generated/intl/messages_zh.dart +++ b/lib/generated/intl/messages_zh.dart @@ -336,6 +336,8 @@ class MessageLookup extends MessageLookupByLibrary { "cannotAddMorePhotosAfterBecomingViewer": m7, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage("无法删除共享文件"), + "castInstruction": MessageLookupByLibrary.simpleMessage( + "在您要配对的设备上访问 cast.ente.io。\n输入下面的代码即可在电视上播放相册。"), "centerPoint": MessageLookupByLibrary.simpleMessage("中心点"), "changeEmail": MessageLookupByLibrary.simpleMessage("修改邮箱"), "changeLocationOfSelectedItems": @@ -468,10 +470,12 @@ class MessageLookup extends MessageLookupByLibrary { "details": MessageLookupByLibrary.simpleMessage("详情"), "devAccountChanged": MessageLookupByLibrary.simpleMessage( "我们用于在 App Store 上发布 ente 的开发者账户已更改。 因此,您将需要重新登录。\n\n对于给您带来的不便,我们深表歉意,但这是不可避免的。"), + "deviceCodeHint": MessageLookupByLibrary.simpleMessage("输入代码"), "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage("添加到此设备相册的文件将自动上传到 ente。"), "deviceLockExplanation": MessageLookupByLibrary.simpleMessage( "当 ente 在前台并且正在进行备份时禁用设备屏幕锁定。 这通常不需要,但可以帮助大型库的大上传和初始导入更快地完成。"), + "deviceNotFound": MessageLookupByLibrary.simpleMessage("未发现设备"), "didYouKnow": MessageLookupByLibrary.simpleMessage("您知道吗?"), "disableAutoLock": MessageLookupByLibrary.simpleMessage("禁用自动锁定"), "disableDownloadWarningBody": @@ -662,6 +666,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("项目显示永久删除前剩余的天数"), "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage("所选项目将从此相册中移除"), + "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "keepPhotos": MessageLookupByLibrary.simpleMessage("保留照片"), "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("公里"), "kindlyHelpUsWithThisInformation": @@ -805,6 +810,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("可选的,按您喜欢的短语..."), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("或者选择一个现有的"), + "pair": MessageLookupByLibrary.simpleMessage("配对"), "password": MessageLookupByLibrary.simpleMessage("密码"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("密码修改成功"), @@ -832,6 +838,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("您添加的照片将从相册中移除"), "pickCenterPoint": MessageLookupByLibrary.simpleMessage("选择中心点"), "pinAlbum": MessageLookupByLibrary.simpleMessage("置顶相册"), + "playOnTv": MessageLookupByLibrary.simpleMessage("在电视上播放相册"), "playStoreFreeTrialValidTill": m37, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore 订阅"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index e882159ed..701a01164 100644 --- a/lib/generated/l10n.dart +++ b/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 { return Intl.message( - 'We have preserved over 10 million memories so far', + 'We have preserved over 30 million memories so far', name: 'loadMessage2', desc: '', args: [], @@ -8307,6 +8307,66 @@ class S { 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 { diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 6a71af50f..519dc2871 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -10,5 +10,6 @@ "selectALocation": "Select a location", "selectALocationFirst": "Select a location first", "changeLocationOfSelectedItems": "Change location of selected items?", - "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente" + "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente", + "joinDiscord": "Join Discord" } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index f64a91832..0e99c86f8 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -965,7 +965,7 @@ "didYouKnow": "Schon gewusst?", "loadingMessage": "Fotos werden geladen...", "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", "loadMessage4": "Alle unsere Apps sind Open-Source", "loadMessage5": "Unser Quellcode und unsere Kryptografie wurden extern geprüft", @@ -1178,5 +1178,6 @@ "selectALocationFirst": "Wähle zuerst einen Standort", "changeLocationOfSelectedItems": "Standort der gewählten Elemente ändern?", "editsToLocationWillOnlyBeSeenWithinEnte": "Änderungen des Standorts werden nur in ente sichtbar sein", - "cleanUncategorized": "Unkategorisiert leeren" + "cleanUncategorized": "Unkategorisiert leeren", + "joinDiscord": "Join Discord" } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index ae31c07d6..fee87d311 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -974,7 +974,7 @@ "didYouKnow": "Did you know?", "loadingMessage": "Loading your photos...", "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", "loadMessage4": "All our apps are open source", "loadMessage5": "Our source code and cryptography have been externally audited", @@ -1187,5 +1187,11 @@ "selectALocationFirst": "Select a location first", "changeLocationOfSelectedItems": "Change location of selected items?", "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" +} \ No newline at end of file diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 0720f8d3e..d9f69970f 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -897,7 +897,7 @@ "didYouKnow": "¿Sabías que?", "loadingMessage": "Cargando tus fotos...", "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", "loadMessage4": "Todas nuestras aplicaciones son de código abierto", "loadMessage5": "Nuestro código fuente y criptografía han sido auditados externamente", @@ -973,5 +973,6 @@ "selectALocation": "Select a location", "selectALocationFirst": "Select a location first", "changeLocationOfSelectedItems": "Change location of selected items?", - "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente" + "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente", + "joinDiscord": "Join Discord" } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index fe9c4c64c..87d792c7b 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -948,7 +948,7 @@ "didYouKnow": "Le savais-tu ?", "loadingMessage": "Chargement de vos photos...", "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", "loadMessage4": "Toutes nos applications sont open source", "loadMessage5": "Notre code source et notre cryptographie ont été audités en externe", @@ -1154,5 +1154,6 @@ "selectALocation": "Select a location", "selectALocationFirst": "Select a location first", "changeLocationOfSelectedItems": "Change location of selected items?", - "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente" + "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente", + "joinDiscord": "Join Discord" } \ No newline at end of file diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 7df3892f5..83fa1d054 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -948,7 +948,7 @@ "didYouKnow": "Lo sapevi che?", "loadingMessage": "Caricando le tue foto...", "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", "loadMessage4": "Tutte le nostre app sono open source", "loadMessage5": "Il nostro codice sorgente e la crittografia hanno ricevuto audit esterni", @@ -1116,5 +1116,6 @@ "selectALocation": "Select a location", "selectALocationFirst": "Select a location first", "changeLocationOfSelectedItems": "Change location of selected items?", - "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente" + "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente", + "joinDiscord": "Join Discord" } \ No newline at end of file diff --git a/lib/l10n/intl_ko.arb b/lib/l10n/intl_ko.arb index 6a71af50f..519dc2871 100644 --- a/lib/l10n/intl_ko.arb +++ b/lib/l10n/intl_ko.arb @@ -10,5 +10,6 @@ "selectALocation": "Select a location", "selectALocationFirst": "Select a location first", "changeLocationOfSelectedItems": "Change location of selected items?", - "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente" + "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente", + "joinDiscord": "Join Discord" } \ No newline at end of file diff --git a/lib/l10n/intl_nl.arb b/lib/l10n/intl_nl.arb index d12585168..ef9d9e433 100644 --- a/lib/l10n/intl_nl.arb +++ b/lib/l10n/intl_nl.arb @@ -1187,5 +1187,11 @@ "selectALocationFirst": "Selecteer eerst een locatie", "changeLocationOfSelectedItems": "Locatie van geselecteerde items wijzigen?", "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" } \ No newline at end of file diff --git a/lib/l10n/intl_no.arb b/lib/l10n/intl_no.arb index d55787f17..ec336dba8 100644 --- a/lib/l10n/intl_no.arb +++ b/lib/l10n/intl_no.arb @@ -24,5 +24,6 @@ "selectALocation": "Select a location", "selectALocationFirst": "Select a location first", "changeLocationOfSelectedItems": "Change location of selected items?", - "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente" + "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente", + "joinDiscord": "Join Discord" } \ No newline at end of file diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index 7dfb7abc1..183e2b5bd 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -111,5 +111,6 @@ "selectALocation": "Select a location", "selectALocationFirst": "Select a location first", "changeLocationOfSelectedItems": "Change location of selected items?", - "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente" + "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente", + "joinDiscord": "Join Discord" } \ No newline at end of file diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index e0232c58e..8fe4d999b 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -277,5 +277,6 @@ "selectALocation": "Select a location", "selectALocationFirst": "Select a location first", "changeLocationOfSelectedItems": "Change location of selected items?", - "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente" + "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente", + "joinDiscord": "Join Discord" } \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 730c2bf5e..8bc770f2d 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -1187,5 +1187,11 @@ "selectALocationFirst": "首先选择一个位置", "changeLocationOfSelectedItems": "确定要更改所选项目的位置吗?", "editsToLocationWillOnlyBeSeenWithinEnte": "对位置的编辑只能在 Ente 内看到", - "cleanUncategorized": "清除未分类的" + "cleanUncategorized": "清除未分类的", + "playOnTv": "在电视上播放相册", + "pair": "配对", + "deviceNotFound": "未发现设备", + "castInstruction": "在您要配对的设备上访问 cast.ente.io。\n输入下面的代码即可在电视上播放相册。", + "deviceCodeHint": "输入代码", + "joinDiscord": "Join Discord" } \ No newline at end of file diff --git a/lib/services/collections_service.dart b/lib/services/collections_service.dart index fc51ced3c..3ea85746c 100644 --- a/lib/services/collections_service.dart +++ b/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> share( int collectionID, String email, diff --git a/lib/services/location_service.dart b/lib/services/location_service.dart index 703d96b03..78c3dcf36 100644 --- a/lib/services/location_service.dart +++ b/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_tag/location_tag.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:shared_preferences/shared_preferences.dart"; @@ -32,9 +31,7 @@ class LocationService { void init(SharedPreferences preferences) { prefs = preferences; - if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) { - _loadCities(); - } + _loadCities(); } Future>> _getStoredLocationTags() async { diff --git a/lib/services/update_service.dart b/lib/services/update_service.dart index 851e7be43..2a63d818b 100644 --- a/lib/services/update_service.dart +++ b/lib/services/update_service.dart @@ -16,7 +16,7 @@ class UpdateService { static final UpdateService instance = UpdateService._privateConstructor(); static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key"; static const changeLogVersionKey = "update_change_log_key"; - static const currentChangeLogVersion = 13; + static const currentChangeLogVersion = 14; LatestVersionInfo? _latestVersion; final _logger = Logger("UpdateService"); diff --git a/lib/ui/components/info_item_widget.dart b/lib/ui/components/info_item_widget.dart index 9f8169fc0..5bec95ccf 100644 --- a/lib/ui/components/info_item_widget.dart +++ b/lib/ui/components/info_item_widget.dart @@ -8,6 +8,7 @@ class InfoItemWidget extends StatelessWidget { final IconData leadingIcon; final VoidCallback? editOnTap; final String? title; + final Widget? endSection; final Future> subtitleSection; final bool hasChipButtons; final VoidCallback? onTap; @@ -15,6 +16,7 @@ class InfoItemWidget extends StatelessWidget { required this.leadingIcon, this.editOnTap, this.title, + this.endSection, required this.subtitleSection, this.hasChipButtons = false, this.onTap, @@ -70,6 +72,9 @@ class InfoItemWidget extends StatelessWidget { ), ), ]); + + endSection != null ? children.add(endSection!) : null; + return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/ui/map/enable_map.dart b/lib/ui/map/enable_map.dart index 320c7b033..770137023 100644 --- a/lib/ui/map/enable_map.dart +++ b/lib/ui/map/enable_map.dart @@ -48,3 +48,9 @@ Future requestForMapEnable(BuildContext context) async { } return false; } + +//For debugging. +void disableMap() { + UserRemoteFlagService.instance + .setBoolValue(UserRemoteFlagService.mapEnabled, false); +} diff --git a/lib/ui/map/map_marker.dart b/lib/ui/map/map_marker.dart index 076c168d8..0009370be 100644 --- a/lib/ui/map/map_marker.dart +++ b/lib/ui/map/map_marker.dart @@ -2,20 +2,28 @@ import "package:flutter/material.dart"; import "package:flutter_map/flutter_map.dart"; import "package:latlong2/latlong.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"; -Marker mapMarker(ImageMarker imageMarker, String key) { +Marker mapMarker( + ImageMarker imageMarker, + String key, { + Size markerSize = MapView.defaultMarkerSize, +}) { 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), - width: 75, - height: 75, + width: markerSize.width, + height: markerSize.height, point: LatLng( imageMarker.latitude, imageMarker.longitude, ), builder: (context) => MarkerImage( file: imageMarker.imageFile, - seperator: 85, + seperator: (MapView.defaultMarkerSize.height + 10) - + (MapView.defaultMarkerSize.height - markerSize.height), ), ); } diff --git a/lib/ui/map/map_screen.dart b/lib/ui/map/map_screen.dart index 0a021c045..adb2d590d 100644 --- a/lib/ui/map/map_screen.dart +++ b/lib/ui/map/map_screen.dart @@ -2,6 +2,7 @@ import "dart:async"; import "dart:isolate"; import "package:collection/collection.dart"; +import "package:computer/computer.dart"; import "package:flutter/foundation.dart"; import 'package:flutter/material.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> final Future> Function() filesFutureFn; + final LatLng? center; + final double initialZoom; const MapScreen({ super.key, required this.filesFutureFn, + this.center, + this.initialZoom = 4.5, }); @override @@ -41,11 +46,10 @@ class _MapScreenState extends State { StreamController>.broadcast(); MapController mapController = MapController(); bool isLoading = true; - double initialZoom = 4.5; double maxZoom = 18.0; double minZoom = 2.8; int debounceDuration = 500; - LatLng center = const LatLng(46.7286, 4.8614); + late LatLng center; final Logger _logger = Logger("_MapScreenState"); StreamSubscription? _mapMoveSubscription; Isolate? isolate; @@ -67,6 +71,7 @@ class _MapScreenState extends State { Future initialize() async { try { + center = widget.center ?? const LatLng(46.7286, 4.8614); allImages = await widget.filesFutureFn(); unawaited(processFiles(allImages)); } catch (e, s) { @@ -75,47 +80,25 @@ class _MapScreenState extends State { } Future processFiles(List files) async { - final List 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()}', + final result = await Computer.shared().compute( + _findRecentFileAndGenerateTempMarkers, + param: {"files": files, "center": widget.center}, + ); + + final EnteFile? mostRecentFile = result.$1; + final List tempMarkers = result.$2; + + if (tempMarkers.isNotEmpty) { + center = widget.center ?? + LatLng( + mostRecentFile!.location!.latitude!, + mostRecentFile.location!.longitude!, ); - continue; - } - hasAnyLocation = true; - 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, - ), - ); - } - } - - if (hasAnyLocation) { - center = LatLng( - mostRecentFile!.location!.latitude!, - mostRecentFile.location!.longitude!, - ); if (kDebugMode) { - debugPrint("Info for map: center $center, initialZoom $initialZoom"); + debugPrint( + "Info for map: center $center, initialZoom ${widget.initialZoom}", + ); } } else { showShortToast(context, S.of(context).noImagesWithLocation); @@ -127,7 +110,7 @@ class _MapScreenState extends State { mapController.move( center, - initialZoom, + widget.initialZoom, ); Timer(Duration(milliseconds: debounceDuration), () { @@ -163,6 +146,50 @@ class _MapScreenState extends State { }); } + static (EnteFile?, List) _findRecentFileAndGenerateTempMarkers( + Map args, + ) { + final Logger logger = Logger("_MapScreenState"); + final files = args["files"] as List; + final center = args["center"] as LatLng?; + final List 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') static void _calculateMarkersIsolate(MapIsolate message) async { final bounds = message.bounds; @@ -211,10 +238,9 @@ class _MapScreenState extends State { imageMarkers: imageMarkers, updateVisibleImages: calculateVisibleMarkers, center: center, - initialZoom: initialZoom, + initialZoom: widget.initialZoom, minZoom: minZoom, maxZoom: maxZoom, - debounceDuration: debounceDuration, bottomSheetDraggableAreaHeight: bottomSheetDraggableAreaHeight, ), diff --git a/lib/ui/map/map_view.dart b/lib/ui/map/map_view.dart index f316a7719..15b5c1d8b 100644 --- a/lib/ui/map/map_view.dart +++ b/lib/ui/map/map_view.dart @@ -18,8 +18,13 @@ class MapView extends StatefulWidget { final double minZoom; final double maxZoom; final double initialZoom; - final int debounceDuration; 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({ Key? key, @@ -30,8 +35,12 @@ class MapView extends StatefulWidget { required this.minZoom, required this.maxZoom, required this.initialZoom, - required this.debounceDuration, required this.bottomSheetDraggableAreaHeight, + this.mapAttributionOptions = const MapAttributionOptions(), + this.markerSize = MapView.defaultMarkerSize, + this.onTap, + this.interactiveFlags = InteractiveFlag.all, + this.showControls = true, }) : super(key: key); @override @@ -71,6 +80,11 @@ class _MapViewState extends State { FlutterMap( mapController: widget.controller, options: MapOptions( + onTap: widget.onTap != null + ? (_, __) { + widget.onTap!.call(); + } + : null, center: widget.center, minZoom: widget.minZoom, maxZoom: widget.maxZoom, @@ -85,13 +99,16 @@ class _MapViewState extends State { onChange(position.bounds!); } }, + interactiveFlags: widget.interactiveFlags, ), nonRotatedChildren: [ Padding( padding: EdgeInsets.only( bottom: widget.bottomSheetDraggableAreaHeight, ), - child: const OSMFranceTileAttributes(), + child: OSMFranceTileAttributes( + options: widget.mapAttributionOptions, + ), ), ], children: [ @@ -101,7 +118,7 @@ class _MapViewState extends State { anchorPos: AnchorPos.align(AnchorAlign.top), maxClusterRadius: 100, showPolygon: false, - size: const Size(75, 75), + size: widget.markerSize, fitBoundsOptions: const FitBoundsOptions( padding: EdgeInsets.all(80), ), @@ -133,47 +150,51 @@ class _MapViewState extends State { ), ], ), - 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 { List _buildMakers() { return List.generate(widget.imageMarkers.length, (index) { final imageMarker = widget.imageMarkers[index]; - return mapMarker(imageMarker, index.toString()); + return mapMarker( + imageMarker, + index.toString(), + markerSize: widget.markerSize, + ); }); } } diff --git a/lib/ui/map/tile/attribution/map_attribution.dart b/lib/ui/map/tile/attribution/map_attribution.dart index a5e572a34..e00e1a3e6 100644 --- a/lib/ui/map/tile/attribution/map_attribution.dart +++ b/lib/ui/map/tile/attribution/map_attribution.dart @@ -5,7 +5,9 @@ import "dart:async"; import "package:flutter/material.dart"; import "package:flutter_map/plugin_api.dart"; import "package:photos/extensions/list.dart"; +import "package:photos/theme/colors.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 class MapAttributionWidget extends StatefulWidget { @@ -87,6 +89,8 @@ class MapAttributionWidget extends StatefulWidget { /// /// Read the documentation on the individual properties for more information /// and customizability. + + final double iconSize; const MapAttributionWidget({ super.key, required this.attributions, @@ -99,6 +103,7 @@ class MapAttributionWidget extends StatefulWidget { this.showFlutterMapAttribution = true, this.animationConfig = const FadeRAWA(), this.popupInitialDisplayDuration = Duration.zero, + this.iconSize = 20, }); @override @@ -168,27 +173,23 @@ class MapAttributionWidgetState extends State { duration: widget.animationConfig.buttonDuration, child: popupExpanded ? (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, () => setState(() => popupExpanded = false), ) : (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, () { diff --git a/lib/ui/map/tile/layers.dart b/lib/ui/map/tile/layers.dart index 4a60415a4..2597dddea 100644 --- a/lib/ui/map/tile/layers.dart +++ b/lib/ui/map/tile/layers.dart @@ -9,6 +9,18 @@ import "package:url_launcher/url_launcher_string.dart"; 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 { const OSMTileLayer({super.key}); @@ -42,28 +54,37 @@ class OSMFranceTileLayer extends StatelessWidget { } class OSMFranceTileAttributes extends StatelessWidget { - const OSMFranceTileAttributes({super.key}); + final MapAttributionOptions options; + const OSMFranceTileAttributes({ + this.options = const MapAttributionOptions(), + super.key, + }); @override Widget build(BuildContext context) { + final textTheme = getEnteTextTheme(context).tinyBold; return MapAttributionWidget( alignment: AttributionAlignment.bottomLeft, showFlutterMapAttribution: false, + permanentHeight: options.permanentHeight, + popupBackgroundColor: getEnteColorScheme(context).backgroundElevated, + popupBorderRadius: options.popupBorderRadius, + iconSize: options.iconSize, attributions: [ TextSourceAttribution( S.of(context).openstreetmapContributors, - textStyle: getEnteTextTheme(context).smallBold, + textStyle: textTheme, onTap: () => launchUrlString('https://openstreetmap.org/copyright'), ), TextSourceAttribution( 'HOT Tiles', - textStyle: getEnteTextTheme(context).smallBold, + textStyle: textTheme, onTap: () => launchUrl(Uri.parse('https://www.hotosm.org/')), ), TextSourceAttribution( S.of(context).hostedAtOsmFrance, + textStyle: textTheme, onTap: () => launchUrl(Uri.parse('https://www.openstreetmap.fr/')), - textStyle: getEnteTextTheme(context).smallBold, ), ], ); diff --git a/lib/ui/notification/update/change_log_page.dart b/lib/ui/notification/update/change_log_page.dart index 3e41db1bf..ba7f24f3e 100644 --- a/lib/ui/notification/update/change_log_page.dart +++ b/lib/ui/notification/update/change_log_page.dart @@ -1,3 +1,5 @@ +import "dart:async"; + import 'package:flutter/material.dart'; import "package:photos/generated/l10n.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/title_bar_title_widget.dart'; import 'package:photos/ui/notification/update/change_log_entry.dart'; +import "package:url_launcher/url_launcher_string.dart"; class ChangeLogPage extends StatefulWidget { const ChangeLogPage({ @@ -81,13 +84,28 @@ class _ChangeLogPageState extends State { ButtonWidget( buttonType: ButtonType.trailingIconSecondary, buttonSize: ButtonSize.large, - labelText: S.of(context).rateTheApp, - icon: Icons.favorite_rounded, + labelText: S.of(context).joinDiscord, + icon: Icons.discord_outlined, iconColor: enteColorScheme.primary500, 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), ], ), @@ -102,13 +120,18 @@ class _ChangeLogPageState extends State { Widget _getChangeLog() { final scrollController = ScrollController(); final List items = []; - items.add( + items.addAll([ 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( padding: const EdgeInsets.only(left: 16), diff --git a/lib/ui/viewer/file/file_details_widget.dart b/lib/ui/viewer/file/file_details_widget.dart index 8c39cc927..f8e7abb8e 100644 --- a/lib/ui/viewer/file/file_details_widget.dart +++ b/lib/ui/viewer/file/file_details_widget.dart @@ -145,6 +145,7 @@ class _FileDetailsWidgetState extends State { }, ), ); + fileDetailsTiles.addAll([ ValueListenableBuilder( valueListenable: hasLocationData, diff --git a/lib/ui/viewer/file/video_widget_new.dart b/lib/ui/viewer/file/video_widget_new.dart index 0968f9577..018a6f658 100644 --- a/lib/ui/viewer/file/video_widget_new.dart +++ b/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_video/media_kit_video.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/models/file/extensions/file_props.dart"; import "package:photos/models/file/file.dart"; @@ -43,6 +45,7 @@ class _VideoWidgetNewState extends State final _progressNotifier = ValueNotifier(null); late StreamSubscription playingStreamSubscription; bool _isAppInFG = true; + late StreamSubscription pauseVideoSubscription; @override void initState() { @@ -83,6 +86,10 @@ class _VideoWidgetNewState extends State widget.playbackCallback!(event); } }); + + pauseVideoSubscription = Bus.instance.on().listen((event) { + player.pause(); + }); } @override @@ -96,6 +103,7 @@ class _VideoWidgetNewState extends State @override void dispose() { + pauseVideoSubscription.cancel(); removeCallBack(widget.file); _progressNotifier.dispose(); WidgetsBinding.instance.removeObserver(this); diff --git a/lib/ui/viewer/file_details/albums_item_widget.dart b/lib/ui/viewer/file_details/albums_item_widget.dart index 7f0219474..eae8c5d3d 100644 --- a/lib/ui/viewer/file_details/albums_item_widget.dart +++ b/lib/ui/viewer/file_details/albums_item_widget.dart @@ -1,6 +1,8 @@ import "package:flutter/material.dart"; import "package:logging/logging.dart"; +import "package:photos/core/event_bus.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/models/collection/collection.dart'; import 'package:photos/models/collection/collection_items.dart'; @@ -87,6 +89,7 @@ class AlbumsItemWidget extends StatelessWidget { if (c.isHidden()) { return; } + Bus.instance.fire(PauseVideoEvent()); routeToPage( context, CollectionPage( diff --git a/lib/ui/viewer/file_details/file_properties_item_widget.dart b/lib/ui/viewer/file_details/file_properties_item_widget.dart index 5fd923f8d..a92b99374 100644 --- a/lib/ui/viewer/file_details/file_properties_item_widget.dart +++ b/lib/ui/viewer/file_details/file_properties_item_widget.dart @@ -51,13 +51,13 @@ class _FilePropertiesItemWidgetState extends State { final StringBuffer dimString = StringBuffer(); if (widget.exifData["resolution"] != null && widget.exifData["megaPixels"] != null) { - dimString.write('${widget.exifData["megaPixels"]}MP '); + dimString.write('${widget.exifData["megaPixels"]}MP '); dimString.write('${widget.exifData["resolution"]}'); } else if (widget.file.hasDimensions) { final double megaPixels = (widget.file.width * widget.file.height) / 1000000; 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}'); } final subSectionWidgets = []; diff --git a/lib/ui/viewer/file_details/location_tags_widget.dart b/lib/ui/viewer/file_details/location_tags_widget.dart index 713e39ecc..731c7b89c 100644 --- a/lib/ui/viewer/file_details/location_tags_widget.dart +++ b/lib/ui/viewer/file_details/location_tags_widget.dart @@ -1,15 +1,28 @@ import "dart:async"; +import "dart:ui"; 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/events/location_tag_updated_event.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/models/file/file.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/theme/ente_theme.dart"; import "package:photos/ui/components/buttons/chip_button_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/location_screen.dart"; import "package:photos/utils/navigation_util.dart"; @@ -29,13 +42,19 @@ class _LocationTagsWidgetState extends State { late Future> locationTagChips; late StreamSubscription _locTagUpdateListener; VoidCallback? onTap; + bool _loadedLocationTags = false; + @override void initState() { - locationTagChips = _getLocationTags(); + locationTagChips = _getLocationTags().then((value) { + _loadedLocationTags = true; + return value; + }); _locTagUpdateListener = Bus.instance.on().listen((event) { locationTagChips = _getLocationTags(); }); + super.initState(); } @@ -58,6 +77,9 @@ class _LocationTagsWidgetState extends State { subtitleSection: locationTagChips, hasChipButtons: hasChipButtons ?? true, onTap: onTap, + endSection: _loadedLocationTags + ? InfoMap(widget.file) + : const SizedBox.shrink(), /// to be used when state issues are fixed when location is updated // editOnTap: widget.file.ownerID == Configuration.instance.getUserID()! @@ -83,6 +105,7 @@ class _LocationTagsWidgetState extends State { } Future> _getLocationTags() async { + // await Future.delayed(const Duration(seconds: 1)); final locationTags = await LocationService.instance .enclosingLocationTags(widget.file.location!); if (locationTags.isEmpty) { @@ -139,3 +162,206 @@ class _LocationTagsWidgetState extends State { } } } + +class InfoMap extends StatefulWidget { + final EnteFile file; + const InfoMap(this.file, {super.key}); + + @override + State createState() => _InfoMapState(); +} + +class _InfoMapState extends State { + 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; + }); + }), + ); + } +} diff --git a/lib/ui/viewer/gallery/component/group/lazy_group_gallery.dart b/lib/ui/viewer/gallery/component/group/lazy_group_gallery.dart index b5580ca3c..8e81a4eb7 100644 --- a/lib/ui/viewer/gallery/component/group/lazy_group_gallery.dart +++ b/lib/ui/viewer/gallery/component/group/lazy_group_gallery.dart @@ -56,8 +56,7 @@ class _LazyGroupGalleryState extends State { late Logger _logger; - late List _files; - Set? _filesAsSet; + late List _filesInGroup; late StreamSubscription? _reloadEventSubscription; late StreamSubscription _currentIndexSubscription; bool? _shouldRender; @@ -65,7 +64,8 @@ class _LazyGroupGalleryState extends State { @override void initState() { super.initState(); - _areAllFromGroupSelectedNotifier = ValueNotifier(_areAllFromGroupSelected()); + _areAllFromGroupSelectedNotifier = + ValueNotifier(_areAllFromGroupSelected()); widget.selectedFiles?.addListener(_selectedFilesListener); _showSelectAllButtonNotifier = ValueNotifier(widget.showSelectAllByDefault); @@ -75,7 +75,7 @@ class _LazyGroupGalleryState extends State { void _init() { _logger = Logger("LazyLoading_${widget.logTag}"); _shouldRender = true; - _files = widget.files; + _filesInGroup = widget.files; _areAllFromGroupSelectedNotifier.value = _areAllFromGroupSelected(); _reloadEventSubscription = widget.reloadEvent?.listen((e) => _onReload(e)); @@ -91,11 +91,6 @@ class _LazyGroupGalleryState extends State { }); } - Set get _setOfFiles { - _filesAsSet ??= _files.toSet(); - return _filesAsSet!; - } - bool _areAllFromGroupSelected() { if (widget.selectedFiles != null && widget.selectedFiles!.files.length >= widget.files.length) { @@ -106,11 +101,11 @@ class _LazyGroupGalleryState extends State { } Future _onReload(FilesUpdatedEvent event) async { - if (_files.isEmpty) { + if (_filesInGroup.isEmpty) { return; } 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 final anyCandidateForGroup = event.updatedFiles.any((file) { final fileDate = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); @@ -152,7 +147,7 @@ class _LazyGroupGalleryState extends State { final galleryState = context.findAncestorStateOfType(); if (galleryState?.mounted ?? false) { galleryState!.setState(() {}); - _files = result.files; + _filesInGroup = result.files; } } else if (kDebugMode) { debugPrint("Unexpected event ${event.type.name}"); @@ -172,7 +167,7 @@ class _LazyGroupGalleryState extends State { @override void didUpdateWidget(LazyGroupGallery oldWidget) { super.didUpdateWidget(oldWidget); - if (!listEquals(_files, widget.files)) { + if (!listEquals(_filesInGroup, widget.files)) { _reloadEventSubscription?.cancel(); _init(); } @@ -180,7 +175,7 @@ class _LazyGroupGalleryState extends State { @override Widget build(BuildContext context) { - if (_files.isEmpty) { + if (_filesInGroup.isEmpty) { return const SizedBox.shrink(); } return Column( @@ -190,7 +185,7 @@ class _LazyGroupGalleryState extends State { children: [ if (widget.enableFileGrouping) GroupHeaderWidget( - timestamp: _files[0].creationTime!, + timestamp: _filesInGroup[0].creationTime!, gridSize: widget.photoGridSize, ), Expanded(child: Container()), @@ -226,7 +221,7 @@ class _LazyGroupGalleryState extends State { ), onTap: () { widget.selectedFiles?.toggleGroupSelection( - _setOfFiles, + _filesInGroup.toSet(), ); }, ); @@ -237,7 +232,7 @@ class _LazyGroupGalleryState extends State { _shouldRender! ? GroupGallery( photoGridSize: widget.photoGridSize, - files: _files, + files: _filesInGroup, tag: widget.tag, asyncLoader: widget.asyncLoader, selectedFiles: widget.selectedFiles, @@ -246,7 +241,7 @@ class _LazyGroupGalleryState extends State { // todo: perf eval should we have separate PlaceHolder for Groups // instead of creating a large cached view : PlaceHolderGridViewWidget( - _files.length, + _filesInGroup.length, widget.photoGridSize, ), ], @@ -256,7 +251,7 @@ class _LazyGroupGalleryState extends State { void _selectedFilesListener() { if (widget.selectedFiles == null) return; _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 if (widget.selectedFiles!.files.isEmpty && !widget.showSelectAllByDefault) { diff --git a/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index de471597a..ab1b15e04 100644 --- a/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/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:logging/logging.dart'; import 'package:photos/core/configuration.dart'; +import "package:photos/core/constants.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/events/subscription_purchased_event.dart'; +import "package:photos/gateways/cast_gw.dart"; import "package:photos/generated/l10n.dart"; +import "package:photos/l10n/l10n.dart"; import 'package:photos/models/backup_status.dart'; import 'package:photos/models/collection/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/navigation_util.dart'; import 'package:photos/utils/toast_util.dart'; +import "package:uuid/uuid.dart"; class GalleryAppBarWidget extends StatefulWidget { final GalleryType type; @@ -64,6 +69,7 @@ enum AlbumPopupAction { ownedArchive, sharedArchive, ownedHide, + playOnTv, sort, leave, freeUpSpace, @@ -472,6 +478,22 @@ class _GalleryAppBarWidgetState extends State { ), ); } + 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()) { items.add( @@ -579,6 +601,8 @@ class _GalleryAppBarWidgetState extends State { await _removeQuickLink(); } else if (value == AlbumPopupAction.leave) { await _leaveAlbum(context); + } else if (value == AlbumPopupAction.playOnTv) { + await castAlbum(); } else if (value == AlbumPopupAction.freeUpSpace) { await _deleteBackedUpFiles(context); } else if (value == AlbumPopupAction.setCover) { @@ -797,4 +821,40 @@ class _GalleryAppBarWidgetState extends State { ); setState(() {}); } + + Future 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); + } + }, + ); + } } diff --git a/lib/utils/exif_util.dart b/lib/utils/exif_util.dart index aa7ed4887..58cbb8c9e 100644 --- a/lib/utils/exif_util.dart +++ b/lib/utils/exif_util.dart @@ -11,6 +11,11 @@ import 'package:photos/utils/file_util.dart'; const kDateTimeOriginal = "EXIF DateTimeOriginal"; const kImageDateTime = "Image DateTime"; +const kExifOffSetKeys = [ + "EXIF OffsetTime", + "EXIF OffsetTimeOriginal", + "EXIF OffsetTimeDigitized", +]; const kExifDateTimePattern = "yyyy:MM:dd HH:mm:ss"; const kEmptyExifDateTime = "0000:00:00 00:00:00"; @@ -56,7 +61,14 @@ Future getCreationTimeFromEXIF( ? exif[kImageDateTime]!.printable : null; 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) { _logger.severe("failed to getCreationTimeFromEXIF", e); @@ -64,6 +76,32 @@ Future getCreationTimeFromEXIF( return null; } +DateTime getDateTimeInDeviceTimezone(String exifTime, String? offsetString) { + final DateTime result = DateFormat(kExifDateTimePattern).parse(exifTime); + if (offsetString == null) { + return result; + } + try { + final List 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 exif) { try { return gpsDataFromExif(exif).toLocationObj(); diff --git a/pubspec.yaml b/pubspec.yaml index badac8992..bc54c46ba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ description: ente photos application # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.8.49+569 +version: 0.8.55+575 publish_to: none environment: