Merge branch 'main' into homewidget
This commit is contained in:
commit
95588c8aef
48 changed files with 822 additions and 175 deletions
12
CHANGELOG.md
12
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
|
||||
|
||||
|
|
3
lib/events/pause_video_event.dart
Normal file
3
lib/events/pause_video_event.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
import "package:photos/events/event.dart";
|
||||
|
||||
class PauseVideoEvent extends Event {}
|
50
lib/gateways/cast_gw.dart
Normal file
50
lib/gateways/cast_gw.dart
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
1
lib/generated/intl/messages_cs.dart
generated
1
lib/generated/intl/messages_cs.dart
generated
|
@ -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"),
|
||||
|
|
3
lib/generated/intl/messages_de.dart
generated
3
lib/generated/intl/messages_de.dart
generated
|
@ -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(
|
||||
|
|
11
lib/generated/intl/messages_en.dart
generated
11
lib/generated/intl/messages_en.dart
generated
|
@ -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"),
|
||||
|
|
3
lib/generated/intl/messages_es.dart
generated
3
lib/generated/intl/messages_es.dart
generated
|
@ -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(
|
||||
|
|
3
lib/generated/intl/messages_fr.dart
generated
3
lib/generated/intl/messages_fr.dart
generated
|
@ -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(
|
||||
|
|
3
lib/generated/intl/messages_it.dart
generated
3
lib/generated/intl/messages_it.dart
generated
|
@ -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(
|
||||
|
|
1
lib/generated/intl/messages_ko.dart
generated
1
lib/generated/intl/messages_ko.dart
generated
|
@ -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"),
|
||||
|
|
10
lib/generated/intl/messages_nl.dart
generated
10
lib/generated/intl/messages_nl.dart
generated
|
@ -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"),
|
||||
|
|
1
lib/generated/intl/messages_no.dart
generated
1
lib/generated/intl/messages_no.dart
generated
|
@ -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":
|
||||
|
|
1
lib/generated/intl/messages_pl.dart
generated
1
lib/generated/intl/messages_pl.dart
generated
|
@ -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ę"),
|
||||
|
|
1
lib/generated/intl/messages_pt.dart
generated
1
lib/generated/intl/messages_pt.dart
generated
|
@ -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"),
|
||||
|
|
7
lib/generated/intl/messages_zh.dart
generated
7
lib/generated/intl/messages_zh.dart
generated
|
@ -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 订阅"),
|
||||
|
|
64
lib/generated/l10n.dart
generated
64
lib/generated/l10n.dart
generated
|
@ -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> {
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -1187,5 +1187,11 @@
|
|||
"selectALocationFirst": "首先选择一个位置",
|
||||
"changeLocationOfSelectedItems": "确定要更改所选项目的位置吗?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "对位置的编辑只能在 Ente 内看到",
|
||||
"cleanUncategorized": "清除未分类的"
|
||||
"cleanUncategorized": "清除未分类的",
|
||||
"playOnTv": "在电视上播放相册",
|
||||
"pair": "配对",
|
||||
"deviceNotFound": "未发现设备",
|
||||
"castInstruction": "在您要配对的设备上访问 cast.ente.io。\n输入下面的代码即可在电视上播放相册。",
|
||||
"deviceCodeHint": "输入代码",
|
||||
"joinDiscord": "Join Discord"
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -48,3 +48,9 @@ Future<bool> requestForMapEnable(BuildContext context) async {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//For debugging.
|
||||
void disableMap() {
|
||||
UserRemoteFlagService.instance
|
||||
.setBoolValue(UserRemoteFlagService.mapEnabled, false);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
() {
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -145,6 +145,7 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
|||
},
|
||||
),
|
||||
);
|
||||
|
||||
fileDetailsTiles.addAll([
|
||||
ValueListenableBuilder(
|
||||
valueListenable: hasLocationData,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>[];
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Add table
Reference in a new issue