Merge branch 'main' into homewidget

This commit is contained in:
Prateek Sunal 2024-02-07 23:36:29 +05:30 committed by GitHub
commit 95588c8aef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 822 additions and 175 deletions

View file

@ -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

View file

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

50
lib/gateways/cast_gw.dart Normal file
View file

@ -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
}
}
}

View file

@ -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"),

View file

@ -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(

View file

@ -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"),

View file

@ -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(

View file

@ -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(

View file

@ -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(

View file

@ -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"),

View file

@ -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"),

View file

@ -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":

View file

@ -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ę"),

View file

@ -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"),

View file

@ -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 订阅"),

View file

@ -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<S> {

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

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

View file

@ -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(
int collectionID,
String email,

View file

@ -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<Iterable<LocalEntity<LocationTag>>> _getStoredLocationTags() async {

View file

@ -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");

View file

@ -8,6 +8,7 @@ class InfoItemWidget extends StatelessWidget {
final IconData leadingIcon;
final VoidCallback? editOnTap;
final String? title;
final Widget? endSection;
final Future<List<Widget>> 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,

View file

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

View file

@ -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),
),
);
}

View file

@ -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<List<File>>
final Future<List<EnteFile>> 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<MapScreen> {
StreamController<List<EnteFile>>.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<MapScreen> {
Future<void> 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<MapScreen> {
}
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()}',
final result = await Computer.shared().compute(
_findRecentFileAndGenerateTempMarkers,
param: {"files": files, "center": widget.center},
);
final EnteFile? mostRecentFile = result.$1;
final List<ImageMarker> 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<MapScreen> {
mapController.move(
center,
initialZoom,
widget.initialZoom,
);
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')
static void _calculateMarkersIsolate(MapIsolate message) async {
final bounds = message.bounds;
@ -211,10 +238,9 @@ class _MapScreenState extends State<MapScreen> {
imageMarkers: imageMarkers,
updateVisibleImages: calculateVisibleMarkers,
center: center,
initialZoom: initialZoom,
initialZoom: widget.initialZoom,
minZoom: minZoom,
maxZoom: maxZoom,
debounceDuration: debounceDuration,
bottomSheetDraggableAreaHeight:
bottomSheetDraggableAreaHeight,
),

View file

@ -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<MapView> {
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<MapView> {
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<MapView> {
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<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() {
return List<Marker>.generate(widget.imageMarkers.length, (index) {
final imageMarker = widget.imageMarkers[index];
return mapMarker(imageMarker, index.toString());
return mapMarker(
imageMarker,
index.toString(),
markerSize: widget.markerSize,
);
});
}
}

View file

@ -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<MapAttributionWidget> {
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,
() {

View file

@ -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,
),
],
);

View file

@ -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<ChangeLogPage> {
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<ChangeLogPage> {
Widget _getChangeLog() {
final scrollController = ScrollController();
final List<ChangeLogEntry> 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),

View file

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

View file

@ -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<VideoWidgetNew>
final _progressNotifier = ValueNotifier<double?>(null);
late StreamSubscription<bool> playingStreamSubscription;
bool _isAppInFG = true;
late StreamSubscription<PauseVideoEvent> pauseVideoSubscription;
@override
void initState() {
@ -83,6 +86,10 @@ class _VideoWidgetNewState extends State<VideoWidgetNew>
widget.playbackCallback!(event);
}
});
pauseVideoSubscription = Bus.instance.on<PauseVideoEvent>().listen((event) {
player.pause();
});
}
@override
@ -96,6 +103,7 @@ class _VideoWidgetNewState extends State<VideoWidgetNew>
@override
void dispose() {
pauseVideoSubscription.cancel();
removeCallBack(widget.file);
_progressNotifier.dispose();
WidgetsBinding.instance.removeObserver(this);

View file

@ -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(

View file

@ -51,13 +51,13 @@ class _FilePropertiesItemWidgetState extends State<FilePropertiesItemWidget> {
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 = <Widget>[];

View file

@ -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<LocationTagsWidget> {
late Future<List<Widget>> locationTagChips;
late StreamSubscription<LocationTagUpdatedEvent> _locTagUpdateListener;
VoidCallback? onTap;
bool _loadedLocationTags = false;
@override
void initState() {
locationTagChips = _getLocationTags();
locationTagChips = _getLocationTags().then((value) {
_loadedLocationTags = true;
return value;
});
_locTagUpdateListener =
Bus.instance.on<LocationTagUpdatedEvent>().listen((event) {
locationTagChips = _getLocationTags();
});
super.initState();
}
@ -58,6 +77,9 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
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<LocationTagsWidget> {
}
Future<List<Widget>> _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<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;
});
}),
);
}
}

View file

@ -56,8 +56,7 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
late Logger _logger;
late List<EnteFile> _files;
Set<EnteFile>? _filesAsSet;
late List<EnteFile> _filesInGroup;
late StreamSubscription<FilesUpdatedEvent>? _reloadEventSubscription;
late StreamSubscription<int> _currentIndexSubscription;
bool? _shouldRender;
@ -65,7 +64,8 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
@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<LazyGroupGallery> {
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<LazyGroupGallery> {
});
}
Set<EnteFile> 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<LazyGroupGallery> {
}
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<LazyGroupGallery> {
final galleryState = context.findAncestorStateOfType<GalleryState>();
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<LazyGroupGallery> {
@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<LazyGroupGallery> {
@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<LazyGroupGallery> {
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<LazyGroupGallery> {
),
onTap: () {
widget.selectedFiles?.toggleGroupSelection(
_setOfFiles,
_filesInGroup.toSet(),
);
},
);
@ -237,7 +232,7 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
_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<LazyGroupGallery> {
// 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<LazyGroupGallery> {
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) {

View file

@ -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<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()) {
items.add(
@ -579,6 +601,8 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
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<GalleryAppBarWidget> {
);
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);
}
},
);
}
}

View file

@ -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<DateTime?> 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<DateTime?> getCreationTimeFromEXIF(
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) {
try {
return gpsDataFromExif(exif).toLocationObj();

View file

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