Merge branch 'main' of https://github.com/sv3nnie/ente
This commit is contained in:
@@ -95,8 +95,8 @@ please see our [support guide](SUPPORT.md).
|
||||
<img src=".github/assets/ente-ducky.png" width=200 alt="Ente's Mascot, Ducky,
|
||||
inviting people to Ente's source code repository" />
|
||||
|
||||
Please visit our [community page](https://ente.io/community) for all the ways to
|
||||
connect with the community.
|
||||
Please visit the [community section](https://ente.io/about#community) for all the ways to
|
||||
connect with our community.
|
||||
|
||||
[](https://discord.gg/z2YVKkycX3)
|
||||
[](https://ente.io/blog/rss.xml)
|
||||
|
||||
@@ -379,6 +379,14 @@
|
||||
{
|
||||
"title": "Fastmail"
|
||||
},
|
||||
{
|
||||
"title": "Federal Student Aid",
|
||||
"slug": "federal_student_aid",
|
||||
"altNames": [
|
||||
"FSA",
|
||||
"FAFSA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Fidelity",
|
||||
"slug": "fidelity",
|
||||
@@ -958,6 +966,10 @@
|
||||
{
|
||||
"title": "RuneMate"
|
||||
},
|
||||
{
|
||||
"title": "RuneScape Wiki",
|
||||
"slug": "runescape_wiki"
|
||||
},
|
||||
{
|
||||
"title": "Rust Language Forum",
|
||||
"slug": "rust_language_forum",
|
||||
|
||||
6
auth/assets/custom-icons/icons/federal_student_aid.svg
Normal file
6
auth/assets/custom-icons/icons/federal_student_aid.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.9 KiB |
7
auth/assets/custom-icons/icons/runescape_wiki.svg
Normal file
7
auth/assets/custom-icons/icons/runescape_wiki.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 35 35">
|
||||
<g fill="#438ab5" fill-rule="evenodd" transform="translate(4 1)">
|
||||
<path d="M11.7311354 23.0557769L9.91249858 23.0557769 8.79846449 26.6069057 6.97030597 23 6.79891611 23 4.98027925 26.6347942 3.85672349 23.0557769 2 23.0557769 4.54228294 29.9814077 4.75175943 30 6.86556772 25.9189907 9.00794098 30 9.22693914 29.9814077 11.7311354 23.0557769zM14.3322795 29.8698539L14.3322795 23.0557769 12.7135975 23.0557769 12.7135975 29.8698539 14.3322795 29.8698539zM22.2084227 29.8698539L19.1900568 26.3001328 22.0560762 23.0557769 20.1422227 23.0557769 17.6951564 26.0212483 17.6951564 23.0557769 16.0764744 23.0557769 16.0764744 29.8698539 17.6951564 29.8698539 17.6951564 26.6812749 20.2564826 29.8698539 22.2084227 29.8698539zM25 29.8698539L25 23.0557769 23.381318 23.0557769 23.381318 29.8698539 25 29.8698539zM24.4742178 8.98009586L24.4742178 5.59616787C24.4732494 5.5136357 24.4163953 5.44228159 24.3362569 5.42252065 23.0272354 5.11977548 21.7162784 4.97854841 20.4033857 4.99883944 18.3648587 5.0303453 15.6405277 6.73541461 15.8150159 9.26543298 15.9313414 10.9521119 16.9379659 12.3146739 18.8348893 13.3531189 21.1050121 14.6587079 22.1112168 16.0505228 21.8535034 17.5285637 21.4669332 19.7456249 19.4833026 20.2699349 18.2011186 20.9636596 19.8933668 21.0568854 21.1108284 20.9541788 21.8535034 20.6555398 23.5576643 19.970275 24.621281 18.4776117 24.8765595 17.2814785 25.5814 13.9788769 23.0921699 12.4640398 21.8535034 11.6272857 20.6148368 10.7905315 18.5555838 9.39712448 18.5555838 8.2423436 18.5555838 7.08756273 19.0354769 6.19945178 20.606059 5.98878728 22.2560942 5.76746561 23.8084838 6.80552306 24.0666162 8.65926511 24.1000214 8.89915966 24.2358886 9.00610324 24.4742178 8.98009586z"/>
|
||||
<path d="M12.1896778,5.73473633 C12.2458703,5.76929923 12.2836806,5.79287044 12.3031088,5.80544997 C13.8305405,6.79444234 14.5459886,7.96859313 14.4494531,9.32790236 C14.3458984,10.7860487 13.4278718,12.1833682 11.6953731,13.5198609 C11.995423,13.6024263 13.0716006,15.2517434 14.923906,18.4678119 C15.9400176,19.5870375 17.2645126,20.0440386 18.8973912,19.8388151 C17.7166822,20.6938532 16.5941307,21.0918329 15.5297368,21.032754 C13.9331458,20.9441357 12.5153495,20.0153267 11.6953731,18.9752651 C10.8753968,17.9352035 9.17647457,14.3916396 8.02078511,13.3656207 C9.24887971,13.3176267 10.0712516,13.0717507 10.4879009,12.6279929 C11.0163711,12.0651387 11.4324817,11.1727564 11.3052905,9.86386602 C11.242381,9.21648063 10.8576813,8.46000935 10.2600254,7.66096138 C10.1677374,7.53757512 10.1984144,7.42387917 10.3520565,7.31987355 C10.8366434,7.01067102 11.3224095,6.50739801 11.8093549,5.81005452 L11.8102004,5.81066099 C11.8971472,5.68944809 12.0629706,5.65600737 12.1900999,5.73404867 Z"/>
|
||||
<path d="M5.46922112,0 C5.93751334,0 6.45488645,0.251926659 6.49405028,0.821037745 C6.52015951,1.20044514 6.40971704,1.46961432 6.16272288,1.62854529 L6.36548563,4.50160863 L9.73880697,4.59010439 C9.80515586,4.59184498 9.86880672,4.61673544 9.91873596,4.66046503 L10.8936611,5.51433494 C11.0118247,5.61782632 11.0237189,5.79751318 10.9202275,5.91567678 C10.9171724,5.91916507 10.9140324,5.92257811 10.9108103,5.92591286 L10.3160188,6.541511 C10.2118589,6.64931459 10.0419078,6.65776756 9.92756462,6.56083181 L9.4018415,6.11514401 L9.4018415,6.11514401 L7.33749093,6.11514401 C6.98986751,6.27375711 6.78712075,6.48688034 6.72925065,6.75451369 C6.67138054,7.02214704 6.66841118,9.38843602 6.72034254,13.8533806 C6.72034254,15.5011837 6.88214839,17.3116009 7.20576008,19.2846324 L5.58460752,21.9888272 L3.70958016,19.2846324 C4.08537518,17.1566151 4.27327269,15.282922 4.27327269,13.6635531 L3.43377358,12.9035744 L4.28218079,12.0252455 C4.31100967,8.71955904 4.31100967,6.96264844 4.28218079,6.75451369 C4.23893746,6.44231156 4.03865152,6.30830705 3.71848826,6.11514401 L1.69132923,6.11514401 L1.15474102,6.5615377 C1.03891569,6.65789407 0.868043682,6.64720638 0.765127903,6.53716821 L0.191996049,5.92437216 C0.0855950374,5.81060756 0.0905023165,5.63241981 0.203003442,5.52468375 L1.09677655,4.66876709 C1.14782548,4.61988037 1.2152487,4.59175365 1.28590527,4.58986886 L4.5946007,4.50160863 L4.5946007,4.50160863 L4.76223107,1.62854529 C4.55067524,1.43081789 4.44489732,1.16164871 4.44489732,0.821037745 C4.44489732,0.310121294 5.0009289,0 5.46922112,0 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -499,6 +499,7 @@
|
||||
"appLockOfflineModeWarning": "Has elegido proceder sin copia de seguridad. Si olvidas el código de desbloqueo de la aplicación, se bloqueará el acceso a sus datos.",
|
||||
"duplicateCodes": "Duplicar códigos",
|
||||
"noDuplicates": "✨ No hay duplicados",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "No tienes códigos duplicados que se puedan borrar",
|
||||
"deduplicateCodes": "Desduplicar códigos",
|
||||
"deselectAll": "Deseleccionar todo",
|
||||
"selectAll": "Seleccionar todo",
|
||||
@@ -509,6 +510,7 @@
|
||||
"supportEnte": "Apoya a <bold-green>ente</bold-green>",
|
||||
"giveUsAStarOnGithub": "Danos una estrella en GitHub",
|
||||
"free5GB": "5 GB gratis en <bold-green>ente</bold-green> Fotos",
|
||||
"loginWithAuthAccount": "Inicia sesión con tu cuenta de Auth",
|
||||
"freeStorageOffer": "10% de descuento en <bold-green>ente</bold-green> fotos",
|
||||
"freeStorageOfferDescription": "Usa el cupón \"AUTH\" para obtener un 10% de descuento en el primer año"
|
||||
}
|
||||
@@ -499,6 +499,7 @@
|
||||
"appLockOfflineModeWarning": "Hai scelto di procedere senza backup. Se dimentichi il tuo codice di blocco dell'app, non potrai più accedere ai tuoi dati.",
|
||||
"duplicateCodes": "Codici duplicati",
|
||||
"noDuplicates": "✨ Nessun doppione",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Non ci sono codici duplicati che possono essere cancellati",
|
||||
"deduplicateCodes": "Codici deduplicati",
|
||||
"deselectAll": "Deselezionare tutti",
|
||||
"selectAll": "Seleziona tutti",
|
||||
|
||||
@@ -499,6 +499,7 @@
|
||||
"appLockOfflineModeWarning": "バックアップなしで進むことを選択しました。アプリロックを忘れると、データにアクセスできなくなります。",
|
||||
"duplicateCodes": "重複コード",
|
||||
"noDuplicates": "✨ 重複なし",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "削除できる重複コードはありません",
|
||||
"deduplicateCodes": "重複コード",
|
||||
"deselectAll": "すべての選択を解除",
|
||||
"selectAll": "すべて選択",
|
||||
|
||||
@@ -499,6 +499,7 @@
|
||||
"appLockOfflineModeWarning": "Pasirinkote tęsti be atsarginių kopijų. Jei pamiršite programos užraktą, jums bus užrakinta prieiga prie duomenų.",
|
||||
"duplicateCodes": "Dubliuoti kodus",
|
||||
"noDuplicates": "✨ Dublikatų nėra",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Neturite dubliuotų kodų, kuriuos būtų galima išvalyti.",
|
||||
"deduplicateCodes": "Atdubliuoti kodus",
|
||||
"deselectAll": "Naikinti visų pasirinkimą",
|
||||
"selectAll": "Pasirinkti viską",
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"account": "അക്കൗണ്ട്",
|
||||
"unlock": "അൺലോക്ക്",
|
||||
"qrCode": "QR കോഡ്",
|
||||
"blog": "ബ്ലോഗ്",
|
||||
"verifyPassword": "പാസ്വേഡ് സ്ഥിരീകരിക്കുക",
|
||||
"recreatePassword": "പാസ്വേഡ് പുനഃസൃഷ്ടിക്കുക",
|
||||
|
||||
@@ -88,6 +88,8 @@
|
||||
"useRecoveryKey": "Uporabi ključ za obnovo",
|
||||
"incorrectPasswordTitle": "Nepravilno geslo",
|
||||
"welcomeBack": "Dobrodošli nazaj!",
|
||||
"emailAlreadyRegistered": "E-poštni naslov je že registriran.",
|
||||
"emailNotRegistered": "E-poštni naslov ni registriran.",
|
||||
"madeWithLoveAtPrefix": "ustvarjeno s ❤️pri ",
|
||||
"supportDevs": "Naročite se na <bold-green>ente</bold-green>, da nas podprete",
|
||||
"supportDiscount": "Uporabite kupon \"AUTH\" za 10% popusta za prvo leto",
|
||||
@@ -156,6 +158,7 @@
|
||||
"twoFactorAuthTitle": "Dvojno preverjanja pristnosti",
|
||||
"passkeyAuthTitle": "Potrditev ključa za dostop (passkey)",
|
||||
"verifyPasskey": "Potrdite ključ za dostop (passkey)",
|
||||
"loginWithTOTP": "Prijava z TOTP",
|
||||
"recoverAccount": "Obnovi račun",
|
||||
"enterRecoveryKeyHint": "Vnesite vaš ključ za obnovitev",
|
||||
"recover": "Obnovi",
|
||||
@@ -257,6 +260,10 @@
|
||||
"areYouSureYouWantToLogout": "Ali ste prepričani, da se želite odjaviti?",
|
||||
"yesLogout": "Ja, odjavi se",
|
||||
"exit": "Izhod",
|
||||
"theme": "Tema",
|
||||
"lightTheme": "Svetla",
|
||||
"darkTheme": "Temna",
|
||||
"systemTheme": "Sistemska",
|
||||
"verifyingRecoveryKey": "Preverjanje ključa za obnovitev",
|
||||
"recoveryKeyVerified": "Ključ za obnovitev preverjen",
|
||||
"recoveryKeySuccessBody": "Odlično! Vaš ključ za obnovitev je veljaven. Hvala za preverjanje.\n\nNe pozabite shraniti varnostno kopijo obnovitvenega ključa.",
|
||||
@@ -327,6 +334,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Po meri",
|
||||
"editOrder": "Uredi vrstni red",
|
||||
"mostFrequentlyUsed": "Pogosto uporabljeni",
|
||||
"mostRecentlyUsed": "Nedavno uporabljeno",
|
||||
"activeSessions": "Aktivne seje",
|
||||
@@ -448,6 +457,8 @@
|
||||
"customEndpoint": "Povezano na {endpoint}",
|
||||
"pinText": "Pripni",
|
||||
"unpinText": "Odpni",
|
||||
"pinnedCodeMessage": "{code} je bila pripeta",
|
||||
"unpinnedCodeMessage": "{code} je bila odpeta",
|
||||
"pinned": "Pripeto",
|
||||
"tags": "Oznake",
|
||||
"createNewTag": "Ustvari novo oznako",
|
||||
@@ -485,5 +496,21 @@
|
||||
"appLockNotEnabled": "Zaklepanje aplikacije ni omogočeno",
|
||||
"appLockNotEnabledDescription": "Prosimo, omogočite zaklepanje aplikacije v Nastavitve > Zaklepanje Aplikacije (Security > App Lock)",
|
||||
"authToViewPasskey": "Da vidite passkey, se overite",
|
||||
"appLockOfflineModeWarning": "Odločili ste se, da boste nadaljevali brez varnostnih kopij. Če boste pozabili geslo za odklepanje aplikacije, bo dostop do vaših podatkov onemogočen."
|
||||
"appLockOfflineModeWarning": "Odločili ste se, da boste nadaljevali brez varnostnih kopij. Če boste pozabili geslo za odklepanje aplikacije, bo dostop do vaših podatkov onemogočen.",
|
||||
"duplicateCodes": "Podvojene kode",
|
||||
"noDuplicates": "✨ Ni duplikatov",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Nimate nobenih podvojenih kod, ki bi jih bilo mogoče izbrisati",
|
||||
"deduplicateCodes": "Dedupliciraj kode",
|
||||
"deselectAll": "Prekliči celoten izbor",
|
||||
"selectAll": "Izberi vse",
|
||||
"deleteDuplicates": "Izbriši dvojnike",
|
||||
"plainHTML": "Navadni HTML",
|
||||
"tellUsWhatYouThink": "Povejte nam kaj mislite",
|
||||
"dropReview": "Napišite oceno v trgovini App/Play Store",
|
||||
"supportEnte": "Podpiraj <bold-green>ente</bold-green>",
|
||||
"giveUsAStarOnGithub": "Dajte nam zvezdico na Githubu",
|
||||
"free5GB": "5 GB zastonj na <bold-green>ente</bold-green> fotografije",
|
||||
"loginWithAuthAccount": "Prijavite se s svojim Auth računom",
|
||||
"freeStorageOffer": "10 % popust na <bold-green>ente</bold-green> fotografije",
|
||||
"freeStorageOfferDescription": "Uporabite kupon \"AUTH\" za 10% popusta za prvo leto"
|
||||
}
|
||||
@@ -267,7 +267,9 @@
|
||||
"verifyingRecoveryKey": "Verifierar återställningsnyckel...",
|
||||
"recoveryKeyVerified": "Återställningsnyckel verifierad",
|
||||
"recoveryKeySuccessBody": "Grymt! Din återställningsnyckel är giltig. Tack för att du verifierade.\n\nKom ihåg att hålla din återställningsnyckel säker med backups.",
|
||||
"invalidRecoveryKey": "Återställningsnyckeln du angav är inte giltig. Kontrollera att den innehåller 24 ord och kontrollera stavningen av varje ord.\n\nOm du har angett en äldre återställningskod, se till att den är 64 tecken lång, och kontrollera var och en av bokstäverna.",
|
||||
"recreatePasswordTitle": "Återskapa lösenord",
|
||||
"recreatePasswordBody": "Denna enhet är inte tillräckligt kraftfull för att verifiera ditt lösenord, men vi kan återskapa det på ett sätt som fungerar med alla enheter.\n\nLogga in med din återställningsnyckel och återskapa ditt lösenord (du kan använda samma igen om du vill).",
|
||||
"invalidKey": "Ogiltig nyckel",
|
||||
"tryAgain": "Försök igen",
|
||||
"viewRecoveryKey": "Visa återställningsnyckel",
|
||||
@@ -279,6 +281,10 @@
|
||||
"copyEmailAddress": "Kopiera e-postadress",
|
||||
"exportLogs": "Exportera loggar",
|
||||
"enterYourRecoveryKey": "Ange din återställningsnyckel",
|
||||
"tempErrorContactSupportIfPersists": "Det ser ut som om något gick fel. Försök igen efter en stund. Om felet kvarstår, vänligen kontakta vår support.",
|
||||
"networkHostLookUpErr": "Det gick inte att ansluta till Ente, kontrollera dina nätverksinställningar och kontakta supporten om felet kvarstår.",
|
||||
"networkConnectionRefusedErr": "Det gick inte att ansluta till Ente, försök igen om en stund. Om felet kvarstår, vänligen kontakta support.",
|
||||
"itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Det ser ut som om något gick fel. Försök igen efter en stund. Om felet kvarstår, vänligen kontakta vår support.",
|
||||
"about": "Om",
|
||||
"weAreOpenSource": "Vi är öppen källkod!",
|
||||
"privacy": "Sekretess",
|
||||
@@ -292,6 +298,7 @@
|
||||
"checking": "Kontrollerar ...",
|
||||
"youAreOnTheLatestVersion": "Du är på den senaste versionen",
|
||||
"warning": "Varning",
|
||||
"exportWarningDesc": "Den exporterade filen innehåller känslig information. Förvara den på ett säkert sätt.",
|
||||
"iUnderStand": "Jag förstår",
|
||||
"@iUnderStand": {
|
||||
"description": "Text for the button to confirm the user understands the warning"
|
||||
@@ -309,28 +316,46 @@
|
||||
}
|
||||
},
|
||||
"sorry": "Tyvärr",
|
||||
"importFailureDesc": "Det gick inte att tolka den valda filen.\nSkriv till support@ente.io om du behöver hjälp!",
|
||||
"pendingSyncs": "Varning",
|
||||
"pendingSyncsWarningBody": "En del av dina koder har inte säkerhetskopierats.\n\nSe till att du har en säkerhetskopia för dessa koder innan du loggar ut.",
|
||||
"checkInboxAndSpamFolder": "Vänligen kontrollera din inkorg (och skräppost) för att slutföra verifieringen",
|
||||
"tapToEnterCode": "Tryck för att ange kod",
|
||||
"resendEmail": "Skicka e-post igen",
|
||||
"weHaveSendEmailTo": "Vi har skickat ett mail till <green>{email}</green>",
|
||||
"@weHaveSendEmailTo": {
|
||||
"description": "Text to indicate that we have sent a mail to the user",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"description": "The email address of the user",
|
||||
"type": "String",
|
||||
"example": "example@ente.io"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Anpassad",
|
||||
"editOrder": "Redigera ordning",
|
||||
"mostFrequentlyUsed": "Ofta använd",
|
||||
"mostRecentlyUsed": "Senast använd",
|
||||
"activeSessions": "Aktiva sessioner",
|
||||
"somethingWentWrongPleaseTryAgain": "Något gick fel, vänligen försök igen",
|
||||
"thisWillLogYouOutOfThisDevice": "Detta kommer att logga ut dig från den här enheten!",
|
||||
"thisWillLogYouOutOfTheFollowingDevice": "Detta kommer att logga ut dig från följande enhet:",
|
||||
"terminateSession": "Avsluta session?",
|
||||
"terminate": "Avsluta",
|
||||
"thisDevice": "Den här enheten",
|
||||
"toResetVerifyEmail": "För att återställa ditt lösenord måste du först bekräfta din e-postadress.",
|
||||
"thisEmailIsAlreadyInUse": "Denna e-postadress används redan",
|
||||
"verificationFailedPleaseTryAgain": "Verifiering misslyckades, vänligen försök igen",
|
||||
"yourVerificationCodeHasExpired": "Din verifieringskod har upphört att gälla",
|
||||
"incorrectCode": "Felaktig kod",
|
||||
"sorryTheCodeYouveEnteredIsIncorrect": "Tyvärr, den kod som du har angett är felaktig",
|
||||
"emailChangedTo": "E-post ändrad till {newEmail}",
|
||||
"authenticationFailedPleaseTryAgain": "Autentisering misslyckades, vänligen försök igen",
|
||||
"authenticationSuccessful": "Autentisering lyckades!",
|
||||
"twofactorAuthenticationSuccessfullyReset": "Tvåfaktorsautentisering återställd",
|
||||
"incorrectRecoveryKey": "Felaktig återställningsnyckel",
|
||||
"theRecoveryKeyYouEnteredIsIncorrect": "Återställningsnyckeln du angav är felaktig",
|
||||
"enterPassword": "Ange lösenord",
|
||||
"selectExportFormat": "Välj exportformat",
|
||||
"encrypted": "Krypterad",
|
||||
@@ -343,6 +368,7 @@
|
||||
"showLargeIcons": "Visa stora ikoner",
|
||||
"compactMode": "Kompakt läge",
|
||||
"shouldHideCode": "Dölj koder",
|
||||
"doubleTapToViewHiddenCode": "Du kan dubbeltrycka på en post för att visa koden",
|
||||
"focusOnSearchBar": "Fokusera på sök vid appstart",
|
||||
"minimizeAppOnCopy": "Minimera appen vid kopiering",
|
||||
"editCodeAuthMessage": "Autentisera för att redigera kod",
|
||||
|
||||
BIN
docs/docs/public/replication.png
Normal file
BIN
docs/docs/public/replication.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
@@ -54,6 +54,9 @@ The same principle applies if you're deploying to your custom domain.
|
||||
|
||||
## Replication
|
||||
|
||||

|
||||
<p align="center">Community contributed diagram of Ente's Replication Process</p>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> As of now, Replication works only if all the 3 storage type
|
||||
> needs are fulfilled (1 Hot, 1 Cold and 1 Glacier Storage).
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import "dart:convert";
|
||||
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/models/location/location.dart";
|
||||
|
||||
const baseRadius = 0.6;
|
||||
|
||||
class BaseLocation {
|
||||
final List<EnteFile> files;
|
||||
int? firstCreationTime;
|
||||
@@ -16,6 +20,54 @@ class BaseLocation {
|
||||
this.lastCreationTime,
|
||||
});
|
||||
|
||||
static List<BaseLocation> decodeJsonToList(
|
||||
String jsonString,
|
||||
Map<int, EnteFile> filesMap,
|
||||
) {
|
||||
final jsonList = jsonDecode(jsonString) as List;
|
||||
return jsonList
|
||||
.map((json) => BaseLocation.fromJson(json, filesMap))
|
||||
.toList();
|
||||
}
|
||||
|
||||
static String encodeListToJson(List<BaseLocation> baseLocations) {
|
||||
final jsonList =
|
||||
baseLocations.map((location) => location.toJson()).toList();
|
||||
return jsonEncode(jsonList);
|
||||
}
|
||||
|
||||
static BaseLocation fromJson(
|
||||
Map<String, dynamic> json,
|
||||
Map<int, EnteFile> filesMap,
|
||||
) {
|
||||
return BaseLocation(
|
||||
(json['fileIDs'] as List).map((e) => filesMap[e]!).toList(),
|
||||
Location(
|
||||
latitude: json['location']['latitude'],
|
||||
longitude: json['location']['longitude'],
|
||||
),
|
||||
json['isCurrentBase'] as bool,
|
||||
firstCreationTime: json['firstCreationTime'] as int?,
|
||||
lastCreationTime: json['lastCreationTime'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'fileIDs': files
|
||||
.where((file) => file.uploadedFileID != null)
|
||||
.map((file) => file.uploadedFileID!)
|
||||
.toList(),
|
||||
'location': {
|
||||
'latitude': location.latitude!,
|
||||
'longitude': location.longitude!,
|
||||
},
|
||||
'isCurrentBase': isCurrentBase,
|
||||
'firstCreationTime': firstCreationTime,
|
||||
'lastCreationTime': lastCreationTime,
|
||||
};
|
||||
}
|
||||
|
||||
int averageCreationTime() {
|
||||
if (firstCreationTime != null && lastCreationTime != null) {
|
||||
return (firstCreationTime! + lastCreationTime!) ~/ 2;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import "dart:convert";
|
||||
|
||||
import "package:photos/models/base_location.dart";
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/models/location/location.dart";
|
||||
import "package:photos/models/memories/people_memory.dart";
|
||||
import "package:photos/models/memories/smart_memory.dart";
|
||||
@@ -20,18 +22,29 @@ class MemoriesCache {
|
||||
final List<ToShowMemory> toShowMemories;
|
||||
final List<PeopleShownLog> peopleShownLogs;
|
||||
final List<TripsShownLog> tripsShownLogs;
|
||||
final List<BaseLocation> baseLocations;
|
||||
|
||||
MemoriesCache({
|
||||
required this.toShowMemories,
|
||||
required this.peopleShownLogs,
|
||||
required this.tripsShownLogs,
|
||||
required this.baseLocations,
|
||||
});
|
||||
|
||||
factory MemoriesCache.fromJson(Map<String, dynamic> json) {
|
||||
factory MemoriesCache.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
Map<int, EnteFile> filesMap,
|
||||
) {
|
||||
return MemoriesCache(
|
||||
toShowMemories: ToShowMemory.decodeJsonToList(json['toShowMemories']),
|
||||
peopleShownLogs: PeopleShownLog.decodeJsonToList(json['peopleShownLogs']),
|
||||
tripsShownLogs: TripsShownLog.decodeJsonToList(json['tripsShownLogs']),
|
||||
baseLocations: json['baseLocations'] != null
|
||||
? BaseLocation.decodeJsonToList(
|
||||
json['baseLocations'],
|
||||
filesMap,
|
||||
)
|
||||
: [],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,6 +53,7 @@ class MemoriesCache {
|
||||
'toShowMemories': ToShowMemory.encodeListToJson(toShowMemories),
|
||||
'peopleShownLogs': PeopleShownLog.encodeListToJson(peopleShownLogs),
|
||||
'tripsShownLogs': TripsShownLog.encodeListToJson(tripsShownLogs),
|
||||
'baseLocations': BaseLocation.encodeListToJson(baseLocations),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,8 +61,11 @@ class MemoriesCache {
|
||||
return jsonEncode(cache.toJson());
|
||||
}
|
||||
|
||||
static MemoriesCache decodeFromJsonString(String jsonString) {
|
||||
return MemoriesCache.fromJson(jsonDecode(jsonString));
|
||||
static MemoriesCache decodeFromJsonString(
|
||||
String jsonString,
|
||||
Map<int, EnteFile> filesMap,
|
||||
) {
|
||||
return MemoriesCache.fromJson(jsonDecode(jsonString), filesMap);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,11 @@ extension SectionTypeExtensions on SectionType {
|
||||
}
|
||||
}
|
||||
|
||||
bool get sortByName => this != SectionType.face && this != SectionType.magic;
|
||||
// TODO: lau: check if we should sort moment again
|
||||
bool get sortByName =>
|
||||
this != SectionType.face &&
|
||||
this != SectionType.magic &&
|
||||
this != SectionType.moment;
|
||||
|
||||
bool get isEmptyCTAVisible {
|
||||
switch (this) {
|
||||
@@ -242,6 +246,7 @@ extension SectionTypeExtensions on SectionType {
|
||||
|
||||
case SectionType.moment:
|
||||
if (flagService.internalUser) {
|
||||
// TODO: lau: remove this whole smart memories and moment altogether
|
||||
return SearchService.instance.smartMemories(context, limit);
|
||||
}
|
||||
return SearchService.instance.getRandomMomentsSearchResults(context);
|
||||
|
||||
@@ -10,6 +10,7 @@ import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/location_tag_updated_event.dart";
|
||||
import "package:photos/extensions/stop_watch.dart";
|
||||
import "package:photos/models/api/entity/type.dart";
|
||||
import "package:photos/models/base_location.dart";
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/models/local_entity_data.dart";
|
||||
import "package:photos/models/location/location.dart";
|
||||
@@ -33,6 +34,8 @@ class LocationService {
|
||||
|
||||
List<City> _cities = [];
|
||||
|
||||
List<BaseLocation> baseLocations = [];
|
||||
|
||||
LocationService(this.prefs) {
|
||||
debugPrint('LocationService constructor');
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
|
||||
@@ -140,26 +140,27 @@ class MemoriesCacheService {
|
||||
// calculate memories for this period and for the next period
|
||||
final now = DateTime.now();
|
||||
final next = now.add(kMemoriesUpdateFrequency);
|
||||
final nowMemories =
|
||||
await smartMemoriesService.calcMemories(now, newCache);
|
||||
final nextMemories =
|
||||
final nowResult = await smartMemoriesService.calcMemories(now, newCache);
|
||||
final nextResult =
|
||||
await smartMemoriesService.calcMemories(next, newCache);
|
||||
w?.log("calculated new memories");
|
||||
for (final nowMemory in nowMemories) {
|
||||
for (final nowMemory in nowResult.memories) {
|
||||
newCache.toShowMemories
|
||||
.add(ToShowMemory.fromSmartMemory(nowMemory, now));
|
||||
}
|
||||
for (final nextMemory in nextMemories) {
|
||||
for (final nextMemory in nextResult.memories) {
|
||||
newCache.toShowMemories
|
||||
.add(ToShowMemory.fromSmartMemory(nextMemory, next));
|
||||
}
|
||||
newCache.baseLocations.addAll(nowResult.baseLocations);
|
||||
w?.log("added memories to cache");
|
||||
final file = File(await _getCachePath());
|
||||
if (!file.existsSync()) {
|
||||
file.createSync(recursive: true);
|
||||
}
|
||||
_cachedMemories =
|
||||
nowMemories.where((memory) => memory.shouldShowNow()).toList();
|
||||
nowResult.memories.where((memory) => memory.shouldShowNow()).toList();
|
||||
locationService.baseLocations = nowResult.baseLocations;
|
||||
await file.writeAsBytes(
|
||||
MemoriesCache.encodeToJsonString(newCache).codeUnits,
|
||||
);
|
||||
@@ -174,8 +175,14 @@ class MemoriesCacheService {
|
||||
}
|
||||
}
|
||||
|
||||
/// WARNING: Use for testing only, TODO: lau: remove later
|
||||
Future<MemoriesCache> debugCacheForTesting() async {
|
||||
final oldCache = await _readCacheFromDisk();
|
||||
final MemoriesCache newCache = _processOldCache(oldCache);
|
||||
return newCache;
|
||||
}
|
||||
|
||||
MemoriesCache _processOldCache(MemoriesCache? oldCache) {
|
||||
final List<ToShowMemory> toShowMemories = [];
|
||||
final List<PeopleShownLog> peopleShownLogs = [];
|
||||
final List<TripsShownLog> tripsShownLogs = [];
|
||||
if (oldCache != null) {
|
||||
@@ -221,9 +228,10 @@ class MemoriesCacheService {
|
||||
}
|
||||
}
|
||||
return MemoriesCache(
|
||||
toShowMemories: toShowMemories,
|
||||
toShowMemories: [],
|
||||
peopleShownLogs: peopleShownLogs,
|
||||
tripsShownLogs: tripsShownLogs,
|
||||
baseLocations: [],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -259,6 +267,7 @@ class MemoriesCacheService {
|
||||
);
|
||||
}
|
||||
}
|
||||
locationService.baseLocations = cache.baseLocations;
|
||||
_logger.info('Processing of disk cache memories done');
|
||||
return memories;
|
||||
} catch (e, s) {
|
||||
@@ -294,8 +303,17 @@ class MemoriesCacheService {
|
||||
_logger.info("No memories cache found");
|
||||
return null;
|
||||
}
|
||||
final allFiles = Set<EnteFile>.from(
|
||||
await SearchService.instance.getAllFilesForSearch(),
|
||||
);
|
||||
final allFileIdsToFile = <int, EnteFile>{};
|
||||
for (final file in allFiles) {
|
||||
if (file.uploadedFileID != null) {
|
||||
allFileIdsToFile[file.uploadedFileID!] = file;
|
||||
}
|
||||
}
|
||||
final jsonString = file.readAsStringSync();
|
||||
return MemoriesCache.decodeFromJsonString(jsonString);
|
||||
return MemoriesCache.decodeFromJsonString(jsonString, allFileIdsToFile);
|
||||
}
|
||||
|
||||
Future<void> clearMemoriesCache() async {
|
||||
|
||||
@@ -15,6 +15,7 @@ import "package:photos/db/ml/db.dart";
|
||||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import "package:photos/extensions/user_extension.dart";
|
||||
import "package:photos/models/api/collection/user.dart";
|
||||
import "package:photos/models/base_location.dart";
|
||||
import 'package:photos/models/collection/collection.dart';
|
||||
import 'package:photos/models/collection/collection_items.dart';
|
||||
import "package:photos/models/file/extensions/file_props.dart";
|
||||
@@ -1048,6 +1049,42 @@ class SearchService {
|
||||
);
|
||||
}
|
||||
}
|
||||
// Add the found base locations from the location/memories service
|
||||
// TODO: lau: Add base location names
|
||||
if (limit == null || tagSearchResults.length < limit) {
|
||||
for (final BaseLocation base in locationService.baseLocations) {
|
||||
final a = (baseRadius * scaleFactor(base.location.latitude!)) /
|
||||
kilometersPerDegree;
|
||||
const b = baseRadius / kilometersPerDegree;
|
||||
tagSearchResults.add(
|
||||
GenericSearchResult(
|
||||
ResultType.location,
|
||||
"Base",
|
||||
base.files,
|
||||
onResultTap: (ctx) {
|
||||
showAddLocationSheet(
|
||||
ctx,
|
||||
base.location,
|
||||
name: "Base",
|
||||
radius: baseRadius,
|
||||
);
|
||||
},
|
||||
hierarchicalSearchFilter: LocationFilter(
|
||||
locationTag: LocationTag(
|
||||
name: "Base",
|
||||
radius: baseRadius,
|
||||
centerPoint: base.location,
|
||||
aSquare: a * a,
|
||||
bSquare: b * b,
|
||||
),
|
||||
occurrence: kMostRelevantFilter,
|
||||
matchedUploadedIDs: filesToUploadedFileIDs(base.files),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (limit == null || tagSearchResults.length < limit) {
|
||||
final results =
|
||||
await locationService.getFilesInCity(filesWithNoLocTag, '');
|
||||
@@ -1193,9 +1230,24 @@ class SearchService {
|
||||
BuildContext context,
|
||||
int? limit,
|
||||
) async {
|
||||
final memories = await memoriesCacheService.getMemories(limit);
|
||||
DateTime calcTime = DateTime.now();
|
||||
// await two seconds to let new page load first
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (limit == null) {
|
||||
final DateTime? pickedTime = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(1900),
|
||||
lastDate: DateTime(2100),
|
||||
);
|
||||
if (pickedTime != null) calcTime = pickedTime;
|
||||
}
|
||||
final cache = await memoriesCacheService.debugCacheForTesting();
|
||||
final memoriesResult = await smartMemoriesService
|
||||
.calcMemories(calcTime, cache, debugSurfaceAll: true);
|
||||
locationService.baseLocations = memoriesResult.baseLocations;
|
||||
final searchResults = <GenericSearchResult>[];
|
||||
for (final memory in memories) {
|
||||
for (final memory in memoriesResult.memories) {
|
||||
final files = Memory.filesFromMemories(memory.memories);
|
||||
searchResults.add(
|
||||
GenericSearchResult(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "dart:async";
|
||||
import "dart:math" show min, max;
|
||||
|
||||
import "package:flutter/foundation.dart" show kDebugMode;
|
||||
import "package:flutter/material.dart";
|
||||
import "package:intl/intl.dart";
|
||||
import "package:logging/logging.dart";
|
||||
@@ -9,6 +10,7 @@ import "package:photos/core/configuration.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/db/memories_db.dart";
|
||||
import "package:photos/db/ml/db.dart";
|
||||
import "package:photos/extensions/stop_watch.dart";
|
||||
import "package:photos/l10n/l10n.dart";
|
||||
import "package:photos/models/base_location.dart";
|
||||
import "package:photos/models/file/extensions/file_props.dart";
|
||||
@@ -34,6 +36,13 @@ import "package:photos/services/machine_learning/ml_result.dart";
|
||||
import "package:photos/services/machine_learning/semantic_search/semantic_search_service.dart";
|
||||
import "package:photos/services/search_service.dart";
|
||||
|
||||
class MemoriesResult {
|
||||
final List<SmartMemory> memories;
|
||||
final List<BaseLocation> baseLocations;
|
||||
|
||||
MemoriesResult(this.memories, this.baseLocations);
|
||||
}
|
||||
|
||||
class SmartMemoriesService {
|
||||
final _logger = Logger("SmartMemoriesService");
|
||||
final _memoriesDB = MemoriesDB.instance;
|
||||
@@ -73,45 +82,58 @@ class SmartMemoriesService {
|
||||
}
|
||||
|
||||
// One general method to get all memories, which calls on internal methods for each separate memory type
|
||||
Future<List<SmartMemory>> calcMemories(
|
||||
Future<MemoriesResult> calcMemories(
|
||||
DateTime now,
|
||||
MemoriesCache oldCache,
|
||||
) async {
|
||||
MemoriesCache oldCache, {
|
||||
bool debugSurfaceAll = false,
|
||||
}) async {
|
||||
try {
|
||||
_logger.finest('calcMemories called with time: $now');
|
||||
final TimeLogger t = TimeLogger(context: "calcMemories");
|
||||
_logger.finest('calcMemories called with time: $now $t');
|
||||
await init();
|
||||
final List<SmartMemory> memories = [];
|
||||
final allFiles = Set<EnteFile>.from(
|
||||
await SearchService.instance.getAllFilesForSearch(),
|
||||
);
|
||||
_seenTimes = await _memoriesDB.getSeenTimes();
|
||||
_logger.finest("All files length: ${allFiles.length}");
|
||||
_logger.finest("All files length: ${allFiles.length} $t");
|
||||
|
||||
final peopleMemories =
|
||||
await _getPeopleResults(allFiles, now, oldCache.peopleShownLogs);
|
||||
final peopleMemories = await _getPeopleResults(
|
||||
allFiles,
|
||||
now,
|
||||
oldCache.peopleShownLogs,
|
||||
surfaceAll: debugSurfaceAll,
|
||||
);
|
||||
_deductUsedMemories(allFiles, peopleMemories);
|
||||
memories.addAll(peopleMemories);
|
||||
_logger.finest("All files length: ${allFiles.length}");
|
||||
_logger.finest("All files length after people: ${allFiles.length} $t");
|
||||
|
||||
// Trip memories
|
||||
final tripMemories = await _getTripsResults(allFiles, now);
|
||||
final (tripMemories, bases) = await _getTripsResults(
|
||||
allFiles,
|
||||
now,
|
||||
oldCache.tripsShownLogs,
|
||||
surfaceAll: debugSurfaceAll,
|
||||
);
|
||||
_deductUsedMemories(allFiles, tripMemories);
|
||||
memories.addAll(tripMemories);
|
||||
_logger.finest("All files length: ${allFiles.length}");
|
||||
_logger.finest("All files length after trips: ${allFiles.length} $t");
|
||||
|
||||
// Time memories
|
||||
final timeMemories = await _onThisDayOrWeekResults(allFiles, now);
|
||||
_deductUsedMemories(allFiles, timeMemories);
|
||||
memories.addAll(timeMemories);
|
||||
_logger.finest("All files length: ${allFiles.length}");
|
||||
_logger.finest("All files length after time: ${allFiles.length} $t");
|
||||
|
||||
// Filler memories
|
||||
final fillerMemories = await _getFillerResults(allFiles, now);
|
||||
_deductUsedMemories(allFiles, fillerMemories);
|
||||
memories.addAll(fillerMemories);
|
||||
return memories;
|
||||
_logger.finest("All files length after filler: ${allFiles.length} $t");
|
||||
return MemoriesResult(memories, bases);
|
||||
} catch (e, s) {
|
||||
_logger.severe("Error calculating smart memories", e, s);
|
||||
return [];
|
||||
return MemoriesResult(<SmartMemory>[], <BaseLocation>[]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,8 +151,10 @@ class SmartMemoriesService {
|
||||
Future<List<PeopleMemory>> _getPeopleResults(
|
||||
Iterable<EnteFile> allFiles,
|
||||
DateTime currentTime,
|
||||
List<PeopleShownLog> shownPeople,
|
||||
) async {
|
||||
List<PeopleShownLog> shownPeople, {
|
||||
bool surfaceAll = false,
|
||||
}) async {
|
||||
final w = (kDebugMode ? EnteWatch('getPeopleResults') : null)?..start();
|
||||
final List<PeopleMemory> memoryResults = [];
|
||||
if (allFiles.isEmpty) return [];
|
||||
final allFileIdsToFile = <int, EnteFile>{};
|
||||
@@ -142,6 +166,7 @@ class SmartMemoriesService {
|
||||
final nowInMicroseconds = currentTime.microsecondsSinceEpoch;
|
||||
final windowEnd =
|
||||
currentTime.add(kMemoriesUpdateFrequency).microsecondsSinceEpoch;
|
||||
w?.log('allFiles setup');
|
||||
|
||||
// Get ordered list of important people (all named, from most to least files)
|
||||
final persons = await PersonService.instance.getPersons();
|
||||
@@ -169,6 +194,7 @@ class SmartMemoriesService {
|
||||
final bFaces = personIdToFaceIDs[b]!.length;
|
||||
return bFaces.compareTo(aFaces);
|
||||
});
|
||||
w?.log('orderedImportantPersonsID setup');
|
||||
|
||||
// Check if the user has assignmed "me"
|
||||
String? meID;
|
||||
@@ -179,6 +205,7 @@ class SmartMemoriesService {
|
||||
break;
|
||||
}
|
||||
}
|
||||
w?.log('meID setup part 1');
|
||||
final bool isMeAssigned = meID != null;
|
||||
Map<int, List<Face>>? meFilesToFaces;
|
||||
if (isMeAssigned) {
|
||||
@@ -187,6 +214,7 @@ class SmartMemoriesService {
|
||||
meFileIDs,
|
||||
);
|
||||
}
|
||||
w?.log('meID setup part 2');
|
||||
|
||||
// Loop through the people and find all memories
|
||||
final Map<String, Map<PeopleMemoryType, PeopleMemory>> personToMemories =
|
||||
@@ -194,10 +222,12 @@ class SmartMemoriesService {
|
||||
for (final personID in orderedImportantPersonsID) {
|
||||
final personFileIDs = personIdToFileIDs[personID]!;
|
||||
final personName = personIdToPerson[personID]!.data.name;
|
||||
w?.log('start with new person $personName');
|
||||
final Map<int, List<Face>> personFilesToFaces =
|
||||
await MLDataDB.instance.getFacesForFileIDs(
|
||||
personFileIDs,
|
||||
);
|
||||
w?.log('personFilesToFaces setup');
|
||||
// Inside people loop, check for spotlight (Most likely every person will have a spotlight)
|
||||
final spotlightFiles = <EnteFile>[];
|
||||
for (final fileID in personFileIDs) {
|
||||
@@ -228,6 +258,7 @@ class SmartMemoriesService {
|
||||
.putIfAbsent(personID, () => {})
|
||||
.putIfAbsent(PeopleMemoryType.spotlight, () => spotlightMemory);
|
||||
}
|
||||
w?.log('spotlight setup');
|
||||
|
||||
// Inside people loop, check for youAndThem
|
||||
if (isMeAssigned && meID != personID) {
|
||||
@@ -258,12 +289,14 @@ class SmartMemoriesService {
|
||||
.putIfAbsent(personID, () => {})
|
||||
.putIfAbsent(PeopleMemoryType.youAndThem, () => youAndThemMemory);
|
||||
}
|
||||
w?.log('youAndThem setup');
|
||||
}
|
||||
|
||||
// Inside people loop, check for doingSomethingTogether
|
||||
if (isMeAssigned && meID != personID) {
|
||||
final vectors = await SemanticSearchService.instance
|
||||
.getClipVectorsForFileIDs(personFileIDs);
|
||||
w?.log('getting clip vectors for doingSomethingTogether');
|
||||
final activityFiles = <EnteFile>[];
|
||||
PeopleActivity lastActivity = PeopleActivity.values.first;
|
||||
activityLoop:
|
||||
@@ -277,6 +310,9 @@ class SmartMemoriesService {
|
||||
}
|
||||
final similarities = await MLComputer.instance
|
||||
.compareEmbeddings(vectors, activityVector);
|
||||
w?.log(
|
||||
'comparing embeddings for doingSomethingTogether and $activity',
|
||||
);
|
||||
for (final fileID in personFileIDs) {
|
||||
final similarity = similarities[fileID];
|
||||
if (similarity == null) continue;
|
||||
@@ -307,6 +343,7 @@ class SmartMemoriesService {
|
||||
() => activityMemory,
|
||||
);
|
||||
}
|
||||
w?.log('doingSomethingTogether setup');
|
||||
}
|
||||
|
||||
// Inside people loop, check for lastTimeYouSawThem
|
||||
@@ -355,16 +392,19 @@ class SmartMemoriesService {
|
||||
() => lastTimeMemory,
|
||||
);
|
||||
}
|
||||
w?.log('lastTimeYouSawThem setup');
|
||||
}
|
||||
|
||||
// // Surface everything just for debug checking
|
||||
// for (final personID in personToMemories.keys) {
|
||||
// for (final memoryType in PeopleMemoryType.values) {
|
||||
// if (personToMemories[personID]!.containsKey(memoryType)) {
|
||||
// memoryResults.add(personToMemories[personID]![memoryType]!);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// Surface everything just for debug checking
|
||||
if (surfaceAll) {
|
||||
for (final personID in personToMemories.keys) {
|
||||
final personMemories = personToMemories[personID]!;
|
||||
for (final memoryType in personMemories.keys) {
|
||||
memoryResults.add(personMemories[memoryType]!);
|
||||
}
|
||||
}
|
||||
return memoryResults;
|
||||
}
|
||||
|
||||
// Loop through the people and check if we should surface anything based on relevancy (bday, last met)
|
||||
personRelevancyLoop:
|
||||
@@ -432,6 +472,7 @@ class SmartMemoriesService {
|
||||
}
|
||||
}
|
||||
}
|
||||
w?.log('relevancy setup');
|
||||
|
||||
// Loop through the people (and memory types) and add based on rotation
|
||||
if (memoryResults.length >= 3) return memoryResults;
|
||||
@@ -471,24 +512,30 @@ class SmartMemoriesService {
|
||||
}
|
||||
if (added > 0) break peopleRotationLoop;
|
||||
}
|
||||
w?.log('rotation setup');
|
||||
|
||||
return memoryResults;
|
||||
}
|
||||
|
||||
Future<List<TripMemory>> _getTripsResults(
|
||||
Future<(List<TripMemory>, List<BaseLocation>)> _getTripsResults(
|
||||
Iterable<EnteFile> allFiles,
|
||||
DateTime currentTime,
|
||||
) async {
|
||||
List<TripsShownLog> shownTrips, {
|
||||
bool surfaceAll = false,
|
||||
}) async {
|
||||
final List<TripMemory> memoryResults = [];
|
||||
final Iterable<LocalEntity<LocationTag>> locationTagEntities =
|
||||
(await locationService.getLocationTags());
|
||||
if (allFiles.isEmpty) return [];
|
||||
if (allFiles.isEmpty) return (<TripMemory>[], <BaseLocation>[]);
|
||||
final nowInMicroseconds = currentTime.microsecondsSinceEpoch;
|
||||
final windowEnd =
|
||||
currentTime.add(kMemoriesUpdateFrequency).microsecondsSinceEpoch;
|
||||
final currentMonth = currentTime.month;
|
||||
final cutOffTime = currentTime.subtract(const Duration(days: 365));
|
||||
|
||||
const tripRadius = 100.0;
|
||||
const overlapRadius = 10.0;
|
||||
|
||||
final Map<LocalEntity<LocationTag>, List<EnteFile>> tagToItemsMap = {};
|
||||
for (int i = 0; i < locationTagEntities.length; i++) {
|
||||
tagToItemsMap[locationTagEntities.elementAt(i)] = [];
|
||||
@@ -496,12 +543,13 @@ class SmartMemoriesService {
|
||||
final List<(List<EnteFile>, Location)> smallRadiusClusters = [];
|
||||
final List<(List<EnteFile>, Location)> wideRadiusClusters = [];
|
||||
// Go through all files and cluster the ones not inside any location tag
|
||||
allFilesLoop:
|
||||
for (EnteFile file in allFiles) {
|
||||
if (!file.hasLocation ||
|
||||
file.uploadedFileID == null ||
|
||||
!file.isOwner ||
|
||||
file.creationTime == null) {
|
||||
continue;
|
||||
continue allFilesLoop;
|
||||
}
|
||||
// Check if the file is inside any location tag
|
||||
bool hasLocationTag = false;
|
||||
@@ -516,42 +564,41 @@ class SmartMemoriesService {
|
||||
}
|
||||
}
|
||||
// Cluster the files not inside any location tag (incremental clustering)
|
||||
if (!hasLocationTag) {
|
||||
// Small radius clustering for base locations
|
||||
bool foundSmallCluster = false;
|
||||
for (final cluster in smallRadiusClusters) {
|
||||
final clusterLocation = cluster.$2;
|
||||
if (isFileInsideLocationTag(
|
||||
clusterLocation,
|
||||
file.location!,
|
||||
0.6,
|
||||
)) {
|
||||
cluster.$1.add(file);
|
||||
foundSmallCluster = true;
|
||||
break;
|
||||
}
|
||||
if (hasLocationTag) continue allFilesLoop;
|
||||
// Small radius clustering for base locations
|
||||
bool addedToExistingSmallCluster = false;
|
||||
for (final cluster in smallRadiusClusters) {
|
||||
final clusterLocation = cluster.$2;
|
||||
if (isFileInsideLocationTag(
|
||||
clusterLocation,
|
||||
file.location!,
|
||||
baseRadius,
|
||||
)) {
|
||||
cluster.$1.add(file);
|
||||
addedToExistingSmallCluster = true;
|
||||
break;
|
||||
}
|
||||
if (!foundSmallCluster) {
|
||||
smallRadiusClusters.add(([file], file.location!));
|
||||
}
|
||||
// Wide radius clustering for trip locations
|
||||
bool foundWideCluster = false;
|
||||
for (final cluster in wideRadiusClusters) {
|
||||
final clusterLocation = cluster.$2;
|
||||
if (isFileInsideLocationTag(
|
||||
clusterLocation,
|
||||
file.location!,
|
||||
100.0,
|
||||
)) {
|
||||
cluster.$1.add(file);
|
||||
foundWideCluster = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!foundWideCluster) {
|
||||
wideRadiusClusters.add(([file], file.location!));
|
||||
}
|
||||
if (!addedToExistingSmallCluster) {
|
||||
smallRadiusClusters.add(([file], file.location!));
|
||||
}
|
||||
// Wide radius clustering for trip locations
|
||||
bool addedToExistingWideCluster = false;
|
||||
for (final cluster in wideRadiusClusters) {
|
||||
final clusterLocation = cluster.$2;
|
||||
if (isFileInsideLocationTag(
|
||||
clusterLocation,
|
||||
file.location!,
|
||||
tripRadius,
|
||||
)) {
|
||||
cluster.$1.add(file);
|
||||
addedToExistingWideCluster = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!addedToExistingWideCluster) {
|
||||
wideRadiusClusters.add(([file], file.location!));
|
||||
}
|
||||
}
|
||||
|
||||
// Identify base locations
|
||||
@@ -577,12 +624,20 @@ class SmartMemoriesService {
|
||||
final lastCreationTime = DateTime.fromMicrosecondsSinceEpoch(
|
||||
creationTimes.last,
|
||||
);
|
||||
if (lastCreationTime.difference(firstCreationTime).inDays < 90) {
|
||||
final daysRange = lastCreationTime.difference(firstCreationTime).inDays;
|
||||
if (daysRange < 90) {
|
||||
continue;
|
||||
}
|
||||
// Check for a minimum average number of days photos are clicked in range
|
||||
final daysRange = lastCreationTime.difference(firstCreationTime).inDays;
|
||||
if (uniqueDays.length < daysRange * 0.1) continue;
|
||||
// Check that there isn't a huge time gap somewhere in the range
|
||||
final int gapThreshold = (daysRange * 0.6).round() * microSecondsInDay;
|
||||
int maxGap = 0;
|
||||
for (int i = 1; i < creationTimes.length; i++) {
|
||||
final gap = creationTimes[i] - creationTimes[i - 1];
|
||||
if (gap > maxGap) maxGap = gap;
|
||||
}
|
||||
if (maxGap > gapThreshold) continue;
|
||||
// Check if it's a current or old base location
|
||||
final bool isCurrent = lastCreationTime.isAfter(
|
||||
DateTime.now().subtract(
|
||||
@@ -604,7 +659,7 @@ class SmartMemoriesService {
|
||||
if (isFileInsideLocationTag(
|
||||
baseLocation.location,
|
||||
location,
|
||||
10.0,
|
||||
overlapRadius,
|
||||
)) {
|
||||
tooClose = true;
|
||||
break;
|
||||
@@ -614,7 +669,7 @@ class SmartMemoriesService {
|
||||
if (isFileInsideLocationTag(
|
||||
tag.item.centerPoint,
|
||||
location,
|
||||
10.0,
|
||||
overlapRadius,
|
||||
)) {
|
||||
tooClose = true;
|
||||
break;
|
||||
@@ -753,25 +808,51 @@ class SmartMemoriesService {
|
||||
}
|
||||
|
||||
// For now for testing let's just surface all base locations
|
||||
for (final baseLocation in baseLocations) {
|
||||
String name = "Base (${baseLocation.isCurrentBase ? 'current' : 'old'})";
|
||||
final String? locationName = _tryFindLocationName(
|
||||
Memory.fromFiles(baseLocation.files, _seenTimes),
|
||||
base: true,
|
||||
);
|
||||
if (locationName != null) {
|
||||
name =
|
||||
"$locationName (Base, ${baseLocation.isCurrentBase ? 'current' : 'old'})";
|
||||
}
|
||||
memoryResults.add(
|
||||
TripMemory(
|
||||
// For now surface these on the location section TODO: lau: remove internal flag title
|
||||
if (surfaceAll) {
|
||||
for (final baseLocation in baseLocations) {
|
||||
String name =
|
||||
"Base (${baseLocation.isCurrentBase ? 'current' : 'old'})";
|
||||
final String? locationName = _tryFindLocationName(
|
||||
Memory.fromFiles(baseLocation.files, _seenTimes),
|
||||
name,
|
||||
0,
|
||||
0,
|
||||
baseLocation.location,
|
||||
),
|
||||
);
|
||||
base: true,
|
||||
);
|
||||
if (locationName != null) {
|
||||
name =
|
||||
"$locationName (Base, ${baseLocation.isCurrentBase ? 'current' : 'old'})";
|
||||
}
|
||||
memoryResults.add(
|
||||
TripMemory(
|
||||
Memory.fromFiles(baseLocation.files, _seenTimes),
|
||||
name,
|
||||
nowInMicroseconds,
|
||||
windowEnd,
|
||||
baseLocation.location,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (final trip in validTrips) {
|
||||
final year = DateTime.fromMicrosecondsSinceEpoch(
|
||||
trip.averageCreationTime(),
|
||||
).year;
|
||||
final String? locationName = _tryFindLocationName(trip.memories);
|
||||
String name = "Trip in $year";
|
||||
if (locationName != null) {
|
||||
name = "Trip to $locationName";
|
||||
} else if (year == currentTime.year - 1) {
|
||||
name = "Last year's trip";
|
||||
}
|
||||
final photoSelection = await _bestSelection(trip.memories);
|
||||
memoryResults.add(
|
||||
trip.copyWith(
|
||||
memories: photoSelection,
|
||||
title: name,
|
||||
firstDateToShow: nowInMicroseconds,
|
||||
lastDateToShow: windowEnd,
|
||||
),
|
||||
);
|
||||
}
|
||||
return (memoryResults, baseLocations);
|
||||
}
|
||||
|
||||
// For now we surface the two most recent trips of current month, and if none, the earliest upcoming redundant trip
|
||||
@@ -844,17 +925,14 @@ class SmartMemoriesService {
|
||||
// Otherwise, if no trips happened in the current month,
|
||||
// look for the earliest upcoming trip in another month that has 3+ trips.
|
||||
else {
|
||||
// TODO lau: make sure the same upcoming trip isn't shown multiple times over multiple months
|
||||
final sortedUpcomingMonths =
|
||||
List<int>.generate(12, (i) => ((currentMonth + i) % 12) + 1);
|
||||
List<int>.generate(6, (i) => ((currentMonth + i) % 12) + 1);
|
||||
checkUpcomingMonths:
|
||||
for (final month in sortedUpcomingMonths) {
|
||||
if (tripsByMonthYear.containsKey(month)) {
|
||||
final List<TripMemory> thatMonthTrips = [];
|
||||
for (final trips in tripsByMonthYear[month]!.values) {
|
||||
for (final trip in trips) {
|
||||
thatMonthTrips.add(trip);
|
||||
}
|
||||
thatMonthTrips.addAll(trips);
|
||||
}
|
||||
if (thatMonthTrips.length >= 3) {
|
||||
// take and use the third earliest trip
|
||||
@@ -862,32 +940,46 @@ class SmartMemoriesService {
|
||||
(a, b) =>
|
||||
a.averageCreationTime().compareTo(b.averageCreationTime()),
|
||||
);
|
||||
final trip = thatMonthTrips[2];
|
||||
final year =
|
||||
DateTime.fromMicrosecondsSinceEpoch(trip.averageCreationTime())
|
||||
.year;
|
||||
final String? locationName = _tryFindLocationName(trip.memories);
|
||||
String name = "Trip in $year";
|
||||
if (locationName != null) {
|
||||
name = "Trip to $locationName";
|
||||
} else if (year == currentTime.year - 1) {
|
||||
name = "Last year's trip";
|
||||
checkPotentialTrips:
|
||||
for (final trip in thatMonthTrips.sublist(2)) {
|
||||
for (final shownTrip in shownTrips) {
|
||||
final distance =
|
||||
calculateDistance(trip.location, shownTrip.location);
|
||||
final shownTripDate = DateTime.fromMicrosecondsSinceEpoch(
|
||||
shownTrip.lastTimeShown,
|
||||
);
|
||||
final shownRecently =
|
||||
currentTime.difference(shownTripDate) < kTripShowTimeout;
|
||||
if (distance < overlapRadius && shownRecently) {
|
||||
continue checkPotentialTrips;
|
||||
}
|
||||
}
|
||||
final year = DateTime.fromMicrosecondsSinceEpoch(
|
||||
trip.averageCreationTime(),
|
||||
).year;
|
||||
final String? locationName = _tryFindLocationName(trip.memories);
|
||||
String name = "Trip in $year";
|
||||
if (locationName != null) {
|
||||
name = "Trip to $locationName";
|
||||
} else if (year == currentTime.year - 1) {
|
||||
name = "Last year's trip";
|
||||
}
|
||||
final photoSelection = await _bestSelection(trip.memories);
|
||||
memoryResults.add(
|
||||
trip.copyWith(
|
||||
memories: photoSelection,
|
||||
title: name,
|
||||
firstDateToShow: nowInMicroseconds,
|
||||
lastDateToShow: windowEnd,
|
||||
),
|
||||
);
|
||||
break checkUpcomingMonths;
|
||||
}
|
||||
final photoSelection = await _bestSelection(trip.memories);
|
||||
memoryResults.add(
|
||||
trip.copyWith(
|
||||
memories: photoSelection,
|
||||
title: name,
|
||||
firstDateToShow: nowInMicroseconds,
|
||||
lastDateToShow: windowEnd,
|
||||
),
|
||||
);
|
||||
break checkUpcomingMonths;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return memoryResults;
|
||||
return (memoryResults, baseLocations);
|
||||
}
|
||||
|
||||
Future<List<TimeMemory>> _onThisDayOrWeekResults(
|
||||
@@ -1221,6 +1313,7 @@ class SmartMemoriesService {
|
||||
int? prefferedSize,
|
||||
}) async {
|
||||
try {
|
||||
final w = (kDebugMode ? EnteWatch('getPeopleResults') : null)?..start();
|
||||
final fileCount = memories.length;
|
||||
final int targetSize = prefferedSize ?? 10;
|
||||
if (fileCount <= targetSize) return memories;
|
||||
@@ -1315,6 +1408,7 @@ class SmartMemoriesService {
|
||||
_logger.finest(
|
||||
'People memories selection done, returning ${finalSelection.length} memories',
|
||||
);
|
||||
w?.log('People memories selection done');
|
||||
return finalSelection;
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error in _bestSelectionPeople', e, s);
|
||||
@@ -1354,9 +1448,9 @@ class SmartMemoriesService {
|
||||
final fileToScore = await MLComputer.instance
|
||||
.compareEmbeddings(vectors, _clipPositiveTextVector!);
|
||||
final fileIdToClip = <int, EmbeddingVector>{};
|
||||
for (final vector in vectors) {
|
||||
fileIdToClip[vector.fileID] = vector;
|
||||
}
|
||||
for (final vector in vectors) {
|
||||
fileIdToClip[vector.fileID] = vector;
|
||||
}
|
||||
|
||||
// Get face scores for each file
|
||||
final fileToFaceCount = <int, int>{};
|
||||
|
||||
@@ -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.9.99+1012
|
||||
version: 0.9.101+1014
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
||||
@@ -42,7 +42,7 @@ func NewOfferController(
|
||||
blackFridayOffers := make(ente.BlackFridayOfferPerCountry)
|
||||
path, err := config.BillingConfigFilePath("black-friday.json")
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting offer config file: %v", err)
|
||||
log.Fatalf("Skipping BF configuration, config file not found: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
|
||||
@@ -88,7 +88,7 @@ func parsePricingFile(fileName string) ente.BillingPlansPerCountry {
|
||||
}
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error reading file %s: %v\n", filePath, err)
|
||||
logrus.Errorf("Skipping payment configuration, pricing data unavailable in config: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user