Merge branch 'main' into mobile_ml_naming
This commit is contained in:
@@ -12,13 +12,13 @@
|
||||
"importScanQrCode": "Skenuoti QR kodą",
|
||||
"qrCode": "QR kodas",
|
||||
"importEnterSetupKey": "Įvesti sąrankos raktą",
|
||||
"importAccountPageTitle": "Įveskite paskyros duomenis",
|
||||
"importAccountPageTitle": "Įvesti paskyros duomenis",
|
||||
"secretCanNotBeEmpty": "Paslaptis negali būti tuščia.",
|
||||
"bothIssuerAndAccountCanNotBeEmpty": "Tiek išdavėjas ir paskyra negali būti tušti.",
|
||||
"incorrectDetails": "Neteisingi duomenys",
|
||||
"pleaseVerifyDetails": "Patikrinkite duomenis ir bandykite dar kartą.",
|
||||
"codeIssuerHint": "Išdavėjas",
|
||||
"codeSecretKeyHint": "Slaptas raktas",
|
||||
"codeSecretKeyHint": "Slaptasis raktas",
|
||||
"secret": "Paslaptis",
|
||||
"all": "Viskas",
|
||||
"notes": "Pastabos",
|
||||
@@ -50,7 +50,7 @@
|
||||
"deleteCodeMessage": "Ar tikrai norite ištrinti šį kodą? Šis veiksmas negrįžtamas.",
|
||||
"trashCode": "Ištuštinti kodą?",
|
||||
"trashCodeMessage": "Ar tikrai norite ištuštinti {account} kodą?",
|
||||
"trash": "Šiukšlinė",
|
||||
"trash": "Ištuštinti",
|
||||
"viewLogsAction": "Peržiūrėti žurnalus",
|
||||
"sendLogsDescription": "Tai nusiųs žurnalo įrašus, kurie padės mums išspręsti jūsų problemą. Nors imamės atsargumo priemonių, kad slaptos informacijos nebūtų įrašoma, raginame jus peržiūrėti šiuos žurnalus prieš bendrinant juos.",
|
||||
"preparingLogsTitle": "Ruošiami žurnalai...",
|
||||
@@ -84,11 +84,11 @@
|
||||
"pleaseWait": "Palaukite...",
|
||||
"generatingEncryptionKeysTitle": "Generuojami šifravimo raktai...",
|
||||
"recreatePassword": "Iš naujo sukurti slaptažodį",
|
||||
"recreatePasswordMessage": "Dabartinis įrenginys nėra pakankamai galingas, kad patvirtintų jūsų slaptažodį, todėl turime jį vieną kartą regeneruoti taip, kad jis veiktų visuose įrenginiuose. \n\nPrisijunkite naudojant atkūrimo raktą ir regeneruokite slaptažodį (jei norite, galite vėl naudoti tą patį).",
|
||||
"recreatePasswordMessage": "Dabartinis įrenginys nėra pakankamai galingas, kad patvirtintų jūsų slaptažodį, todėl turime jį vieną kartą iš naujo sugeneruoti taip, kad jis veiktų visuose įrenginiuose. \n\nPrisijunkite naudodami atkūrimo raktą ir sugeneruokite iš naujo slaptažodį (jei norite, galite vėl naudoti tą patį).",
|
||||
"useRecoveryKey": "Naudoti atkūrimo raktą",
|
||||
"incorrectPasswordTitle": "Neteisingas slaptažodis.",
|
||||
"welcomeBack": "Sveiki sugrįžę!",
|
||||
"madeWithLoveAtPrefix": "sukurta su ❤️ ",
|
||||
"madeWithLoveAtPrefix": "sukurta su ❤️ vietoje ",
|
||||
"supportDevs": "Prenumeruokite <bold-green>„ente“</bold-green>, kad palaikytumėte mus",
|
||||
"supportDiscount": "Naudokite kupono kodą „AUTH“, kad gautumėte 10 % nuolaida pirmiesiems metams",
|
||||
"changeEmail": "Keisti el. paštą",
|
||||
@@ -100,14 +100,14 @@
|
||||
"passwordForDecryptingExport": "Slaptažodis eksportui iššifruoti",
|
||||
"passwordEmptyError": "Slaptažodis negali būti tuščias.",
|
||||
"importFromApp": "Importuoti kodus iš „{appName}“",
|
||||
"importGoogleAuthGuide": "Eksportuokite paskyras iš „Google Authenticator“ į QR kodą naudojant parinktį Perkelti paskyras. Tada naudojant kitą įrenginį nuskenuokite QR kodą.\n\nPatarimas: QR kodą galite nufotografuoti naudojant nešiojamojo kompiuterio internetinę vaizdo kamerą.",
|
||||
"importGoogleAuthGuide": "Eksportuokite paskyras iš „Google Authenticator“ į QR kodą naudodami parinktį Perkelti paskyras. Tada naudojant kitą įrenginį nuskenuokite QR kodą.\n\nPatarimas: QR kodą galite nufotografuoti naudojant nešiojamojo kompiuterio internetinę vaizdo kamerą.",
|
||||
"importSelectJsonFile": "Pasirinkti JSON failą",
|
||||
"importSelectAppExport": "Pasirinkti „{appName}“ eksporto failą",
|
||||
"importEnteEncGuide": "Pasirinkite užšifruotą JSON failą, eksportuotą iš „Ente“",
|
||||
"importRaivoGuide": "Naudokite „Raivo“ nustatymuose esančią parinktį „Export OTPs to Zip archive“ (eksportuoti OTP į ZIP archyvą).\n\nIšskleiskite ZIP failą ir importuokite JSON failą.",
|
||||
"importBitwardenGuide": "Naudokite „Bitwarden“ įrankiuose esančią parinktį Eksportuoti saugyklą ir importuokite nešifruotą JSON failą.",
|
||||
"importAegisGuide": "Naudokite „Aegis“ nustatymuose esančią parinktį Eksportuoti slėptuvę.\n\nJei jūsų saugykla yra užšifruota, turėsite įvesti saugyklos slaptažodį, kad iššifruotumėte saugyklą.",
|
||||
"import2FasGuide": "Naudokite 2FAS parinktį „Settings->2FAS Backup->Export to file“.\n\nJei atsarginė kopija užšifruota, turėsite įvesti slaptažodį, kad iššifruotumėte atsarginę kopiją.",
|
||||
"importAegisGuide": "Naudokite „Aegis“ nustatymuose esančią parinktį Eksportuoti slėptuvę.\n\nJei jūsų saugykla užšifruota, turėsite įvesti saugyklos slaptažodį, kad iššifruotumėte saugyklą.",
|
||||
"import2FasGuide": "Naudokite programoje 2FAS esančią parinktį „Settings->2FAS Backup->Export to file“.\n\nJei atsarginė kopija užšifruota, turėsite įvesti slaptažodį, kad iššifruotumėte atsarginę kopiją.",
|
||||
"importLastpassGuide": "Naudokite „Lastpass Authenticator“ nustatymuose esančią parinktį „Transfer accounts“ (perkelti paskyras) ir paspauskite „Export accounts to file“ (eksportuoti paskyras į failą). Importuokite atsisiųstą JSON failą.",
|
||||
"exportCodes": "Eksportuoti kodus",
|
||||
"importLabel": "Importuoti",
|
||||
@@ -122,7 +122,7 @@
|
||||
"authToChangeYourEmail": "Nustatykite tapatybę, kad pakeistumėte savo el. paštą",
|
||||
"authToChangeYourPassword": "Nustatykite tapatybę, kad pakeistumėte slaptažodį",
|
||||
"authToViewSecrets": "Nustatykite tapatybę, kad peržiūrėtumėte savo paslaptis",
|
||||
"authToInitiateSignIn": "Nustatykite tapatybę, kad pradėtumėte prisijungti prie atsarginės kopijos.",
|
||||
"authToInitiateSignIn": "Nustatykite tapatybę, kad pradėtumėte prisijungti norint kurti atsargines kopijas.",
|
||||
"ok": "Gerai",
|
||||
"cancel": "Atšaukti",
|
||||
"yes": "Taip",
|
||||
@@ -134,7 +134,7 @@
|
||||
"copied": "Nukopijuota",
|
||||
"pleaseTryAgain": "Bandykite dar kartą.",
|
||||
"existingUser": "Esamas naudotojas",
|
||||
"newUser": "Naujas platformoje „Ente“",
|
||||
"newUser": "Naujas sistemoje „Ente“",
|
||||
"delete": "Ištrinti",
|
||||
"enterYourPasswordHint": "Įveskite savo slaptažodį",
|
||||
"forgotPassword": "Pamiršau slaptažodį",
|
||||
@@ -170,7 +170,7 @@
|
||||
"invalidQRCode": "Netinkamas QR kodas.",
|
||||
"noRecoveryKeyTitle": "Neturite atkūrimo rakto?",
|
||||
"enterEmailHint": "Įveskite savo el. pašto adresą",
|
||||
"invalidEmailTitle": "Netinkamas el. pašto adresas.",
|
||||
"invalidEmailTitle": "Netinkamas el. pašto adresas",
|
||||
"invalidEmailMessage": "Įveskite tinkamą el. pašto adresą.",
|
||||
"deleteAccount": "Ištrinti paskyrą",
|
||||
"deleteAccountQuery": "Apgailestausime, kad išeinate. Ar susiduriate su kažkokiomis problemomis?",
|
||||
@@ -195,7 +195,7 @@
|
||||
"viewActiveSessions": "Peržiūrėti aktyvius seansus",
|
||||
"authToViewYourActiveSessions": "Nustatykite tapatybę, kad peržiūrėtumėte savo aktyvius seansus",
|
||||
"searchHint": "Ieškokite...",
|
||||
"search": "Ieškoti",
|
||||
"search": "Paieška",
|
||||
"sorryUnableToGenCode": "Atsiprašome, nepavyksta sugeneruoti {issuerName} kodo.",
|
||||
"noResult": "Nėra rezultatų",
|
||||
"addCode": "Pridėti kodą",
|
||||
@@ -242,15 +242,15 @@
|
||||
"resetPasswordTitle": "Nustatyti slaptažodį iš naujo",
|
||||
"encryptionKeys": "Šifravimo raktai",
|
||||
"passwordWarning": "Šio slaptažodžio nesaugome, todėl jei jį pamiršite, <underline>negalėsime iššifruoti jūsų duomenų</underline>",
|
||||
"enterPasswordToEncrypt": "Įveskite slaptažodį, kurį galime naudoti jūsų duomenims šifruoti",
|
||||
"enterNewPasswordToEncrypt": "Įveskite naują slaptažodį, kurį galime naudoti jūsų duomenims šifruoti",
|
||||
"enterPasswordToEncrypt": "Įveskite slaptažodį, kurį galime naudoti jūsų duomenims užšifruoti",
|
||||
"enterNewPasswordToEncrypt": "Įveskite naują slaptažodį, kurį galime naudoti jūsų duomenims užšifruoti",
|
||||
"passwordChangedSuccessfully": "Slaptažodis sėkmingai pakeistas",
|
||||
"generatingEncryptionKeys": "Generuojami šifravimo raktai...",
|
||||
"continueLabel": "Tęsti",
|
||||
"insecureDevice": "Nesaugus įrenginys",
|
||||
"sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Atsiprašome, šiame įrenginyje nepavyko sugeneruoti saugių raktų.\n\nRegistruokitės iš kito įrenginio.",
|
||||
"howItWorks": "Kaip tai veikia",
|
||||
"ackPasswordLostWarning": "Suprantu, kad jei prarasiu slaptažodį, galiu prarasti savo duomenis, kadangi mano duomenys yra <underline>visapusiškai užšifruota</underline>.",
|
||||
"ackPasswordLostWarning": "Suprantu, kad jei prarasiu slaptažodį, galiu prarasti savo duomenis, kadangi duomenys yra <underline>visapusiškai užšifruota</underline>.",
|
||||
"loginTerms": "Spustelėjus Prisijungti sutinku su <u-terms>paslaugų sąlygomis</u-terms> ir <u-policy> privatumo politika</u-policy>",
|
||||
"logInLabel": "Prisijungti",
|
||||
"logout": "Atsijungti",
|
||||
@@ -262,7 +262,7 @@
|
||||
"recoveryKeySuccessBody": "Puiku! Jūsų atkūrimo raktas tinkamas. Dėkojame už patvirtinimą.\n\nNepamirškite sukurti saugią atkūrimo rakto atsarginę kopiją.",
|
||||
"invalidRecoveryKey": "Įvestas atkūrimo raktas yra netinkamas. Įsitikinkite, kad jame yra 24 žodžiai, ir patikrinkite kiekvieno iš jų rašybą.\n\nJei įvedėte senesnį atkūrimo kodą, įsitikinkite, kad jis yra 64 simbolių ilgio, ir patikrinkite kiekvieną iš jų.",
|
||||
"recreatePasswordTitle": "Iš naujo sukurti slaptažodį",
|
||||
"recreatePasswordBody": "Dabartinis įrenginys nėra pakankamai galingas, kad patvirtintų jūsų slaptažodį, bet mes galime iš naujo sugeneruoti taip, kad jis veiktų su visais įrenginiais.\n\nPrisijunkite naudojant atkūrimo raktą ir sugeneruokite iš naujo slaptažodį (jei norite, galite vėl naudoti tą patį).",
|
||||
"recreatePasswordBody": "Dabartinis įrenginys nėra pakankamai galingas, kad patvirtintų jūsų slaptažodį, bet mes galime iš naujo sugeneruoti taip, kad jis veiktų su visais įrenginiais.\n\nPrisijunkite naudodami atkūrimo raktą ir sugeneruokite iš naujo slaptažodį (jei norite, galite vėl naudoti tą patį).",
|
||||
"invalidKey": "Netinkamas raktas.",
|
||||
"tryAgain": "Bandyti dar kartą",
|
||||
"viewRecoveryKey": "Peržiūrėti atkūrimo raktą",
|
||||
@@ -299,7 +299,7 @@
|
||||
},
|
||||
"authToExportCodes": "Nustatykite tapatybę, kad eksportuotumėte savo kodus",
|
||||
"importSuccessTitle": "Valio!",
|
||||
"importSuccessDesc": "Importavote {count} kodų!",
|
||||
"importSuccessDesc": "Importavote {count} kodų.",
|
||||
"@importSuccessDesc": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -316,7 +316,7 @@
|
||||
"checkInboxAndSpamFolder": "Patikrinkite savo gautieją (ir šlamštą), kad užbaigtumėte patvirtinimą",
|
||||
"tapToEnterCode": "Palieskite, kad įvestumėte kodą",
|
||||
"resendEmail": "Iš naujo siųsti el. laišką",
|
||||
"weHaveSendEmailTo": "Išsiuntėme laišką į <green>{email}</green>",
|
||||
"weHaveSendEmailTo": "Išsiuntėme laišką adresu <green>{email}</green>",
|
||||
"@weHaveSendEmailTo": {
|
||||
"description": "Text to indicate that we have sent a mail to the user",
|
||||
"placeholders": {
|
||||
@@ -338,16 +338,16 @@
|
||||
"thisEmailIsAlreadyInUse": "Šis el. paštas jau naudojamas.",
|
||||
"verificationFailedPleaseTryAgain": "Patvirtinimas nepavyko. Bandykite dar kartą.",
|
||||
"yourVerificationCodeHasExpired": "Jūsų patvirtinimo kodo laikas nebegaliojantis.",
|
||||
"incorrectCode": "Neteisingas kodas.",
|
||||
"incorrectCode": "Neteisingas kodas",
|
||||
"sorryTheCodeYouveEnteredIsIncorrect": "Atsiprašome, įvestas kodas yra neteisingas.",
|
||||
"emailChangedTo": "El. paštas pakeistas į {newEmail}",
|
||||
"authenticationFailedPleaseTryAgain": "Tapatybės nustatymas nepavyko. Bandykite dar kartą.",
|
||||
"authenticationSuccessful": "Tapatybės nustatymas sėkmingas!",
|
||||
"twofactorAuthenticationSuccessfullyReset": "Dvigubas tapatybės nustatymas sėkmingai iš naujo nustatytas",
|
||||
"incorrectRecoveryKey": "Neteisingas atkūrimo raktas.",
|
||||
"twofactorAuthenticationSuccessfullyReset": "Dvigubas tapatybės nustatymas sėkmingai iš naujo nustatytas.",
|
||||
"incorrectRecoveryKey": "Neteisingas atkūrimo raktas",
|
||||
"theRecoveryKeyYouEnteredIsIncorrect": "Įvestas atkūrimo raktas yra neteisingas.",
|
||||
"enterPassword": "Įveskite slaptažodį",
|
||||
"selectExportFormat": "Pasirinkti eksporto formatą",
|
||||
"selectExportFormat": "Pasirinkite eksporto formatą",
|
||||
"exportDialogDesc": "Užšifruoti eksportai bus apsaugoti jūsų pasirinktu slaptažodžiu.",
|
||||
"encrypted": "Užšifruota",
|
||||
"plainText": "Paprastasis tekstas",
|
||||
@@ -361,14 +361,14 @@
|
||||
"showLargeIcons": "Rodyti dideles piktogramas",
|
||||
"compactMode": "Kompaktinis režimas",
|
||||
"shouldHideCode": "Slėpti kodus",
|
||||
"doubleTapToViewHiddenCode": "Galite dvigubai paliesti elementą, kad peržiūrėtumėte kodą",
|
||||
"doubleTapToViewHiddenCode": "Galite dukart paliesti elementą, kad peržiūrėtumėte kodą",
|
||||
"focusOnSearchBar": "Fokusuoti paiešką paleidžiant programą",
|
||||
"confirmUpdatingkey": "Ar tikrai norite atnaujinti slaptąjį raktą?",
|
||||
"minimizeAppOnCopy": "Sumažinti programą kopijuojant",
|
||||
"editCodeAuthMessage": "Nustatykite tapatybę, kad redaguotumėte kodą",
|
||||
"deleteCodeAuthMessage": "Nustatykite tapatybę, kad ištrintumėte kodą",
|
||||
"showQRAuthMessage": "Nustatykite tapatybę, kad būtų rodomas QR kodas",
|
||||
"confirmAccountDeleteTitle": "Patvirtinti paskyros ištrynimą",
|
||||
"confirmAccountDeleteTitle": "Patvirtinkite paskyros ištrynimą",
|
||||
"confirmAccountDeleteMessage": "Ši paskyra susieta su kitomis „Ente“ programomis, jei jas naudojate.\n\nJūsų įkelti duomenys per visas „Ente“ programas bus planuojama ištrinti, o jūsų paskyra bus ištrinta negrįžtamai.",
|
||||
"androidBiometricHint": "Patvirtinkite tapatybę",
|
||||
"@androidBiometricHint": {
|
||||
@@ -406,7 +406,7 @@
|
||||
"@goToSettings": {
|
||||
"description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters."
|
||||
},
|
||||
"androidGoToSettingsDescription": "Biometrinis tapatybės nustatymas jūsų įrenginyje nenustatytas. Eikite į Nustatymai > Sauga ir pridėkite biometrinį tapatybės nustatymą.",
|
||||
"androidGoToSettingsDescription": "Biometrinis tapatybės nustatymas jūsų įrenginyje nenustatytas. Eikite į Nustatymai > Saugumas ir pridėkite biometrinį tapatybės nustatymą.",
|
||||
"@androidGoToSettingsDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure biometric on their device. It shows in a dialog on Android side."
|
||||
},
|
||||
@@ -441,7 +441,7 @@
|
||||
"developerSettings": "Kūrėjo nustatymai",
|
||||
"serverEndpoint": "Serverio galutinis taškas",
|
||||
"invalidEndpoint": "Netinkamas galutinis taškas",
|
||||
"invalidEndpointMessage": "Atsiprašome. Jūsų įvestas galutinis taškas yra netinkamas. Įveskite tinkamą galutinį tašką ir bandykite dar kartą.",
|
||||
"invalidEndpointMessage": "Atsiprašome, įvestas galutinis taškas netinkamas. Įveskite tinkamą galutinį tašką ir bandykite dar kartą.",
|
||||
"endpointUpdatedMessage": "Galutinis taškas sėkmingai atnaujintas",
|
||||
"customEndpoint": "Prijungta prie {endpoint}",
|
||||
"pinText": "Prisegti",
|
||||
@@ -467,7 +467,7 @@
|
||||
"immediately": "Iš karto",
|
||||
"reEnterPassword": "Įveskite slaptažodį iš naujo",
|
||||
"reEnterPin": "Įveskite PIN iš naujo",
|
||||
"next": "Sekantis",
|
||||
"next": "Toliau",
|
||||
"tooManyIncorrectAttempts": "Per daug neteisingų bandymų.",
|
||||
"tapToUnlock": "Palieskite, kad atrakintumėte",
|
||||
"setNewPassword": "Nustatykite naują slaptažodį",
|
||||
@@ -477,7 +477,7 @@
|
||||
"hideContentDescriptioniOS": "Paslepia programos turinį programos perjungiklyje",
|
||||
"autoLockFeatureDescription": "Laikas, po kurio programa užrakinama perkėlus ją į foną",
|
||||
"appLockDescription": "Pasirinkite tarp numatytojo įrenginio užrakinimo ekrano ir pasirinktinio užrakinimo ekrano su PIN kodu arba slaptažodžiu.",
|
||||
"pinLock": "PIN užrakinimas",
|
||||
"pinLock": "PIN užraktas",
|
||||
"enterPin": "Įveskite PIN",
|
||||
"setNewPin": "Nustatykite naują PIN",
|
||||
"importFailureDescNew": "Nepavyko išanalizuoti pasirinkto failo.",
|
||||
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
# See:
|
||||
# https://github.com/electron-userland/electron-builder/issues/4181
|
||||
run: sudo apt-get install libarchive-tools
|
||||
run: sudo apt-get update && sudo apt-get install libarchive-tools
|
||||
|
||||
- name: Build
|
||||
uses: ente-io/action-electron-builder@eff78a1d33bdab4c54ede0e5cdc71e0c2cf803e2
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
## v1.7.7 (Unreleased)
|
||||
|
||||
- Retain JPEG originals even on date modifications.
|
||||
- .
|
||||
|
||||
## v1.7.6
|
||||
|
||||
@@ -64,12 +64,14 @@ videos that you imported. The modifications (e.g. date changes) you make within
|
||||
Ente will be written into a separate metadata JSON file during export so as to
|
||||
not modify the original.
|
||||
|
||||
> There is one exception to this. For JPEG files, the Exif DateTimeOriginal is
|
||||
> changed during export from web or desktop apps. This was done on a customer
|
||||
> request, but in hindsight this has been an incorrect move. We are going to
|
||||
> deprecate this behavior, and will instead provide separate tools (or
|
||||
> functionality within the app itself) for customers who intentionally wish to
|
||||
> modify their originals to reflect the associated metadata JSON.
|
||||
> [!WARNING]
|
||||
>
|
||||
> There used to be one exception to this - for JPEG files, the Exif
|
||||
> DateTimeOriginal was changed during export from web or desktop apps. This was
|
||||
> done on a customer request, but in hindsight this was an incorrect change.
|
||||
>
|
||||
> We have deprecated this behaviour, and the desktop version 1.7.6 is going to
|
||||
> be the last version with this exception.
|
||||
|
||||
As an example: suppose you have `flower.png`. When you export your library, you
|
||||
will end up with:
|
||||
@@ -81,13 +83,36 @@ metadata/flower.png.json
|
||||
|
||||
Ente writes this JSON in the same format as Google Takeout so that if a tool
|
||||
supports Google Takeout import, it should be able to read the JSON written by
|
||||
Ente too
|
||||
Ente too.
|
||||
|
||||
> One small difference is that, to avoid clutter, Ente puts the JSON in the
|
||||
> `metadata/` subfolder, while Google puts it next to the file.<br>
|
||||
>
|
||||
> <br>Ente itself will read it from either place.
|
||||
|
||||
Here is a sample of how the JSON would look:
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "This will be imported as the caption",
|
||||
"creationTime": {
|
||||
"timestamp": "1613532136",
|
||||
"formatted": "17 Feb 2021, 03:22:16 UTC"
|
||||
},
|
||||
"modificationTime": {
|
||||
"timestamp": "1640225957",
|
||||
"formatted": "23 Dec 2021, 02:19:17 UTC"
|
||||
},
|
||||
"geoData": {
|
||||
"latitude": 12.004170700000001,
|
||||
"longitude": 79.8013945
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`photoTakenTime` will be considered as an alias for `creationTime`, and
|
||||
`geoDataExif` will be considered as a fallback for `geoData`.
|
||||
|
||||
### File creation time.
|
||||
|
||||
The photo's data will be preserved verbatim, however when it is written out to
|
||||
|
||||
@@ -48,6 +48,10 @@ albums**.
|
||||
result in the creation of a new album – empty folders (or folders that only
|
||||
contain other folders) will be ignored.
|
||||
|
||||
- In separate album mode, only the leafmost folder name is considered. For
|
||||
example, both `A/B/C/D/x.png` and `1/2/3/D/y.png` will get uploaded into the
|
||||
same Ente album named "D".
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> Ente albums cannot be nested currently. That is, in the **separate album**
|
||||
|
||||
@@ -70,7 +70,7 @@ createuser -s postgres
|
||||
|
||||
## Start Museum
|
||||
|
||||
```
|
||||
```sh
|
||||
export ENTE_DB_USER=postgres
|
||||
cd ente/server
|
||||
go run cmd/museum/main.go
|
||||
@@ -78,11 +78,18 @@ go run cmd/museum/main.go
|
||||
|
||||
For live reloads, install [air](https://github.com/air-verse/air#installation). Then you can just call air after declaring the required environment variables. For example,
|
||||
|
||||
```
|
||||
```sh
|
||||
ENTE_DB_USER=ente_user
|
||||
air
|
||||
```
|
||||
|
||||
## Museum as a background service
|
||||
|
||||
Please check the below links if you want to run Museum as a service, both of them are battle tested.
|
||||
|
||||
1. [How to run museum as a systemd service](https://gist.github.com/mngshm/a0edb097c91d1dc45aeed755af310323)
|
||||
2. [Museum.service](https://github.com/ente-io/ente/blob/23e678889189157ecc389c258267685934b83631/server/scripts/deploy/museum.service#L4)
|
||||
|
||||
Once you are done with setting and running Museum, all you are left to do is run the web app and reverse_proxy it with a webserver. You can check the following resources for Deploying your web app.
|
||||
|
||||
1. [Hosting the Web App](https://help.ente.io/self-hosting/guides/web-app).
|
||||
|
||||
5
infra/workers/data-puller/package.json
Normal file
5
infra/workers/data-puller/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "data-puller",
|
||||
"version": "0.0.0",
|
||||
"private": true
|
||||
}
|
||||
30
infra/workers/data-puller/src/index.ts
Normal file
30
infra/workers/data-puller/src/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Proxy requests for downloading files from object storage.
|
||||
*
|
||||
* Used by museum when replicating.
|
||||
*/
|
||||
|
||||
export default {
|
||||
async fetch(request: Request) {
|
||||
switch (request.method) {
|
||||
case "GET":
|
||||
return handleGET(request);
|
||||
default:
|
||||
console.log(`Unsupported HTTP method ${request.method}`);
|
||||
return new Response(null, { status: 405 });
|
||||
}
|
||||
},
|
||||
} satisfies ExportedHandler;
|
||||
|
||||
const handleGET = async (request: Request) => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Random bots keep trying to pentest causing noise in the logs. If the
|
||||
// request doesn't have a src, we can just safely ignore it.
|
||||
const src = url.searchParams.get("src");
|
||||
if (!src) return new Response(null, { status: 400 });
|
||||
|
||||
const source = atob(src);
|
||||
|
||||
return fetch(source);
|
||||
};
|
||||
1
infra/workers/data-puller/tsconfig.json
Normal file
1
infra/workers/data-puller/tsconfig.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "extends": "../tsconfig.base.json", "include": ["src"] }
|
||||
5
infra/workers/data-puller/wrangler.toml
Normal file
5
infra/workers/data-puller/wrangler.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
name = "data-puller"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-06-14"
|
||||
|
||||
tail_consumers = [{ service = "tail" }]
|
||||
@@ -121,5 +121,87 @@
|
||||
"resetPerson": "Reset person",
|
||||
"areYouSureYouWantToResetThisPerson": "Are you sure you want to reset this person?",
|
||||
"allPersonGroupingWillReset": "All groupings for this person will be reset, and you will lose all suggestions made for this person",
|
||||
"yesResetPerson": "Yes, reset person"
|
||||
"yesResetPerson": "Yes, reset person",
|
||||
"enterCode": "Увядзіце код",
|
||||
"scanCode": "Сканіраваць код",
|
||||
"confirm": "Пацвердзіць",
|
||||
"setupComplete": "Наладжванне завершана",
|
||||
"twofactorAuthenticationPageTitle": "Двухфактарная аўтэнтыфікацыя",
|
||||
"albumOwner": "Уладальнік",
|
||||
"@albumOwner": {
|
||||
"description": "Role of the album owner"
|
||||
},
|
||||
"you": "Вы",
|
||||
"addMore": "Дадаць яшчэ",
|
||||
"@addMore": {
|
||||
"description": "Button text to add more collaborators/viewers"
|
||||
},
|
||||
"viewer": "Праглядальнік",
|
||||
"remove": "Выдаліць",
|
||||
"removeParticipant": "Выдаліць удзельніка",
|
||||
"@removeParticipant": {
|
||||
"description": "menuSectionTitle for removing a participant"
|
||||
},
|
||||
"manage": "Кіраванне",
|
||||
"never": "Ніколі",
|
||||
"after1Hour": "Праз 1 гадзіну",
|
||||
"after1Day": "Праз 1 дзень",
|
||||
"after1Week": "Праз 1 тыдзень",
|
||||
"after1Month": "Праз 1 месяц",
|
||||
"after1Year": "Праз 1 год",
|
||||
"manageParticipants": "Кіраванне",
|
||||
"sendLink": "Адправіць спасылку",
|
||||
"copyLink": "Скапіяваць спасылку",
|
||||
"done": "Гатова",
|
||||
"apply": "Ужыць",
|
||||
"codeAppliedPageTitle": "Код ужыты",
|
||||
"change": "Змяніць",
|
||||
"storageInGB": "{storageAmountInGB} Гб",
|
||||
"details": "Падрабязнасці",
|
||||
"deleteAlbum": "Выдаліць альбом",
|
||||
"yesRemove": "Так, выдаліць",
|
||||
"removeWithQuestionMark": "Выдаліць?",
|
||||
"deletePhotos": "Выдаліць фота",
|
||||
"trash": "Сметніца",
|
||||
"uncategorized": "Без катэгорыі",
|
||||
"videoSmallCase": "відэа",
|
||||
"photoSmallCase": "фота",
|
||||
"deleteFromEnte": "Выдаліць з Ente",
|
||||
"yesDelete": "Так, выдаліць",
|
||||
"magicSearch": "Магічны пошук",
|
||||
"discover_screenshots": "Скрыншоты",
|
||||
"discover_receipts": "Чэкі",
|
||||
"discover_notes": "Нататкі",
|
||||
"discover_pets": "Хатнія жывёлы",
|
||||
"discover_selfies": "Сэлфi",
|
||||
"discover_wallpapers": "Шпалеры",
|
||||
"discover_food": "Ежа",
|
||||
"status": "Стан",
|
||||
"selectAll": "Абраць усё",
|
||||
"skip": "Прапусціць",
|
||||
"about": "Пра праграму",
|
||||
"logout": "Выйсці",
|
||||
"yesLogout": "Так, выйсці",
|
||||
"update": "Абнавіць",
|
||||
"installManually": "Усталяваць уручную",
|
||||
"updateAvailable": "Даступна абнаўленне",
|
||||
"ignoreUpdate": "Iгнараваць",
|
||||
"retry": "Паўтарыць",
|
||||
"backup": "Рэзервовая копія",
|
||||
"removeDuplicates": "Выдаліць дублікаты",
|
||||
"viewLargeFiles": "Вялікія файлы",
|
||||
"noDuplicates": "✨ Няма дублікатаў",
|
||||
"rateUs": "Ацаніце нас",
|
||||
"familyPlans": "Сямейныя тарыфныя планы",
|
||||
"notifications": "Апавяшчэнні",
|
||||
"general": "Асноўныя",
|
||||
"security": "Бяспека",
|
||||
"lockscreen": "Экран блакіроўкі",
|
||||
"support": "Падтрымка",
|
||||
"theme": "Тема",
|
||||
"lightTheme": "Светлая",
|
||||
"darkTheme": "Цёмная",
|
||||
"systemTheme": "Сістэма",
|
||||
"freeTrial": "Бясплатная пробная версія",
|
||||
"faqs": "Частыя пытанні"
|
||||
}
|
||||
@@ -13,9 +13,9 @@
|
||||
"feedback": "Atsiliepimai",
|
||||
"kindlyHelpUsWithThisInformation": "Maloniai padėkite mums su šia informacija",
|
||||
"confirmDeletePrompt": "Taip, noriu negrįžtamai ištrinti šią paskyrą ir jos duomenis per visas programas.",
|
||||
"confirmAccountDeletion": "Patvirtinti paskyros ištrynimą",
|
||||
"confirmAccountDeletion": "Patvirtinkite paskyros ištrynimą",
|
||||
"deleteAccountPermanentlyButton": "Ištrinti paskyrą negrįžtamai",
|
||||
"yourAccountHasBeenDeleted": "Jūsų paskyra buvo ištrinta",
|
||||
"yourAccountHasBeenDeleted": "Jūsų paskyra ištrinta",
|
||||
"selectReason": "Pasirinkite priežastį",
|
||||
"deleteReason1": "Trūksta pagrindinės funkcijos, kurios man reikia",
|
||||
"deleteReason2": "Programa arba tam tikra funkcija nesielgia taip, kaip, mano manymu, turėtų elgtis",
|
||||
@@ -53,7 +53,7 @@
|
||||
"checkInboxAndSpamFolder": "Patikrinkite savo gautieją (ir šlamštą), kad užbaigtumėte patvirtinimą",
|
||||
"tapToEnterCode": "Palieskite, kad įvestumėte kodą",
|
||||
"resendEmail": "Iš naujo siųsti el. laišką",
|
||||
"weHaveSendEmailTo": "Išsiuntėme laišką į <green>{email}</green>",
|
||||
"weHaveSendEmailTo": "Išsiuntėme laišką adresu <green>{email}</green>",
|
||||
"@weHaveSendEmailTo": {
|
||||
"description": "Text to indicate that we have sent a mail to the user",
|
||||
"placeholders": {
|
||||
@@ -94,7 +94,7 @@
|
||||
"sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Atsiprašome, šiame įrenginyje nepavyko sugeneruoti saugių raktų.\n\nRegistruokitės iš kito įrenginio.",
|
||||
"howItWorks": "Kaip tai veikia",
|
||||
"encryption": "Šifravimas",
|
||||
"ackPasswordLostWarning": "Suprantu, kad jei prarasiu slaptažodį, galiu prarasti savo duomenis, kadangi mano duomenys yra <underline>visapusiškai užšifruota</underline>.",
|
||||
"ackPasswordLostWarning": "Suprantu, kad jei prarasiu slaptažodį, galiu prarasti savo duomenis, kadangi mano duomenys yra <underline>visapusiškai užšifruoti</underline>.",
|
||||
"privacyPolicyTitle": "Privatumo politika",
|
||||
"termsOfServicesTitle": "Sąlygos",
|
||||
"signUpTerms": "Sutinku su <u-terms>paslaugų sąlygomis</u-terms> ir <u-policy> privatumo politika</u-policy>",
|
||||
@@ -104,7 +104,7 @@
|
||||
"enterYourPassword": "Įveskite savo slaptažodį",
|
||||
"welcomeBack": "Sveiki sugrįžę!",
|
||||
"contactSupport": "Susisiekti su palaikymo komanda",
|
||||
"incorrectPasswordTitle": "Neteisingas slaptažodis.",
|
||||
"incorrectPasswordTitle": "Neteisingas slaptažodis",
|
||||
"pleaseTryAgain": "Bandykite dar kartą.",
|
||||
"recreatePasswordTitle": "Iš naujo sukurti slaptažodį",
|
||||
"useRecoveryKey": "Naudoti atkūrimo raktą",
|
||||
@@ -129,11 +129,12 @@
|
||||
}
|
||||
},
|
||||
"twofactorSetup": "Dvigubo tapatybės nustatymo sąranka",
|
||||
"enterCode": "Įveskite kodą",
|
||||
"enterCode": "Įvesti kodą",
|
||||
"scanCode": "Skenuoti kodą",
|
||||
"codeCopiedToClipboard": "Nukopijuotas kodas į iškarpinę",
|
||||
"copypasteThisCodentoYourAuthenticatorApp": "Nukopijuokite ir įklijuokite šį kodą\nį autentifikatoriaus programą",
|
||||
"scanThisBarcodeWithnyourAuthenticatorApp": "Skenuokite šį brūkšninį kodą\nsu autentifikatoriaus programa",
|
||||
"tapToCopy": "palieskite, kad nukopijuotumėte",
|
||||
"scanThisBarcodeWithnyourAuthenticatorApp": "Skenuokite šį QR kodą\nsu autentifikatoriaus programa",
|
||||
"enterThe6digitCodeFromnyourAuthenticatorApp": "Įveskite 6 skaitmenų kodą\niš autentifikatoriaus programos",
|
||||
"confirm": "Patvirtinti",
|
||||
"setupComplete": "Sąranka baigta",
|
||||
@@ -157,25 +158,79 @@
|
||||
"orPickAnExistingOne": "Arba pasirinkite esamą",
|
||||
"collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": "Bendradarbiai gali pridėti nuotraukų ir vaizdo įrašų į bendrintą albumą.",
|
||||
"enterEmail": "Įveskite el. paštą",
|
||||
"addMore": "Pridėti daugiau",
|
||||
"@addMore": {
|
||||
"description": "Button text to add more collaborators/viewers"
|
||||
},
|
||||
"remove": "Šalinti",
|
||||
"removeParticipant": "Šalinti dalyvį",
|
||||
"@removeParticipant": {
|
||||
"description": "menuSectionTitle for removing a participant"
|
||||
},
|
||||
"changePermissions": "Keisti leidimus?",
|
||||
"yesConvertToViewer": "Taip, keisti į žiūrėtoją",
|
||||
"cannotAddMorePhotosAfterBecomingViewer": "{user} negalės pridėti daugiau nuotraukų į šį albumą\n\nJie vis tiek galės pašalinti esamas pridėtas nuotraukas",
|
||||
"allowAddingPhotos": "Leisti pridėti nuotraukų",
|
||||
"@allowAddingPhotos": {
|
||||
"description": "Switch button to enable uploading photos to a public link"
|
||||
},
|
||||
"allowAddPhotosDescription": "Leiskite nuorodą turintiems asmenims taip pat pridėti nuotraukų į bendrinamą albumą.",
|
||||
"passwordLock": "Slaptažodžio užraktas",
|
||||
"disableDownloadWarningTitle": "Atkreipkite dėmesį",
|
||||
"disableDownloadWarningBody": "Žiūrėtojai vis tiek gali daryti ekrano kopijas arba išsaugoti nuotraukų kopijas naudojant išorinius įrankius",
|
||||
"allowDownloads": "Leisti atsisiuntimus",
|
||||
"linkDeviceLimit": "Įrenginių riba",
|
||||
"noDeviceLimit": "Jokio",
|
||||
"@noDeviceLimit": {
|
||||
"description": "Text to indicate that there is limit on number of devices"
|
||||
},
|
||||
"linkExpiry": "Nuorodos galiojimo laikas",
|
||||
"linkEnabled": "Įjungta",
|
||||
"linkNeverExpires": "Niekada",
|
||||
"setAPassword": "Nustatyti slaptažodį",
|
||||
"lockButtonLabel": "Užrakinti",
|
||||
"enterPassword": "Įveskite slaptažodį",
|
||||
"removeLink": "Šalinti nuorodą",
|
||||
"manageLink": "Tvarkyti nuorodą",
|
||||
"albumUpdated": "Atnaujintas albumas",
|
||||
"never": "Niekada",
|
||||
"custom": "Pasirinktinis",
|
||||
"@custom": {
|
||||
"description": "Label for setting custom value for link expiry"
|
||||
},
|
||||
"after1Hour": "Po 1 valandos",
|
||||
"after1Day": "Po 1 dienos",
|
||||
"after1Week": "Po 1 savaitės",
|
||||
"after1Month": "Po 1 mėnesio",
|
||||
"after1Year": "Po 1 metų",
|
||||
"manageParticipants": "Tvarkyti",
|
||||
"collabLinkSectionDescription": "Sukurkite nuorodą, kad asmenys galėtų pridėti ir peržiūrėti nuotraukas bendrinamame albume, nereikalaujant „Ente“ programos ar paskyros. Puikiai tinka renginių nuotraukoms rinkti.",
|
||||
"sendLink": "Siųsti nuorodą",
|
||||
"copyLink": "Kopijuoti nuorodą",
|
||||
"emailNoEnteAccount": "{email} neturi „Ente“ paskyros.\n\nSiųskite jiems kvietimą bendrinti nuotraukas.",
|
||||
"applyCodeTitle": "Taikyti kodą",
|
||||
"apply": "Taikyti",
|
||||
"codeAppliedPageTitle": "Pritaikytas kodas",
|
||||
"change": "Keisti",
|
||||
"unavailableReferralCode": "Atsiprašome, šis kodas nepasiekiamas.",
|
||||
"codeChangeLimitReached": "Atsiprašome, pasiekėte kodo pakeitimų ribą.",
|
||||
"storageInGB": "{storageAmountInGB} GB",
|
||||
"faq": "DUK",
|
||||
"total": "iš viso",
|
||||
"removeFromAlbumTitle": "Pašalinti iš albumo?",
|
||||
"removeFromAlbum": "Šalinti iš albumo",
|
||||
"itemsWillBeRemovedFromAlbum": "Pasirinkti elementai bus pašalinti iš šio albumo",
|
||||
"removeShareItemsWarning": "Kai kuriuos elementus, kuriuos šalinate, pridėjo kiti asmenys, todėl prarasite prieigą prie jų",
|
||||
"sorryCouldNotRemoveFromFavorites": "Atsiprašome, nepavyko pašalinti iš mėgstamų.",
|
||||
"subscribeToEnableSharing": "Kad įjungtumėte bendrinimą, reikia aktyvios mokamos prenumeratos.",
|
||||
"subscribe": "Prenumeruoti",
|
||||
"canOnlyRemoveFilesOwnedByYou": "Galima pašalinti tik jums priklausančius failus",
|
||||
"deleteAlbum": "Ištrinti albumą",
|
||||
"deleteAlbumDialog": "Taip pat ištrinti šiame albume esančias nuotraukas (ir vaizdo įrašus) iš <bold>visų</bold> kitų albumų, kuriuose jos yra dalis?",
|
||||
"yesRemove": "Taip, šalinti",
|
||||
"creatingLink": "Kuriama nuoroda...",
|
||||
"removeWithQuestionMark": "Šalinti?",
|
||||
"removeParticipantBody": "{userEmail} bus pašalintas iš šio bendrinamo albumo\n\nVisos jų pridėtos nuotraukos taip pat bus pašalintos iš albumo",
|
||||
"keepPhotos": "Palikti nuotraukas",
|
||||
"deletePhotos": "Ištrinti nuotraukas",
|
||||
"inviteToEnte": "Kviesti į „Ente“",
|
||||
@@ -254,6 +309,21 @@
|
||||
"indexedItems": "Indeksuoti elementai",
|
||||
"pendingItems": "Laukiami elementai",
|
||||
"skip": "Praleisti",
|
||||
"duplicateItemsGroup": "{count} failai (-ų), kiekvienas {formattedSize}",
|
||||
"@duplicateItemsGroup": {
|
||||
"description": "Display the number of duplicate files and their size",
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"example": "12",
|
||||
"type": "int"
|
||||
},
|
||||
"formattedSize": {
|
||||
"example": "2.3 MB",
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"about": "Apie",
|
||||
"weAreOpenSource": "Esame atviro kodo!",
|
||||
"privacy": "Privatumas",
|
||||
@@ -274,6 +344,8 @@
|
||||
"authToInitiateAccountDeletion": "Nustatykite tapatybę, kad pradėtumėte paskyros ištrynimą",
|
||||
"areYouSureYouWantToLogout": "Ar tikrai norite atsijungti?",
|
||||
"yesLogout": "Taip, atsijungti",
|
||||
"removeDuplicates": "Šalinti dublikatus",
|
||||
"youveNoDuplicateFilesThatCanBeCleared": "Neturite dubliuotų failų, kuriuos būtų galima išvalyti",
|
||||
"no": "Ne",
|
||||
"yes": "Taip",
|
||||
"social": "Socialinės",
|
||||
@@ -294,7 +366,25 @@
|
||||
"lightTheme": "Šviesi",
|
||||
"darkTheme": "Tamsi",
|
||||
"systemTheme": "Sistemos",
|
||||
"freeTrial": "Nemokamas bandomasis laikotarpis",
|
||||
"selectYourPlan": "Pasirinkite planą",
|
||||
"enteSubscriptionPitch": "„Ente“ išsaugo jūsų prisiminimus, todėl jie visada bus pasiekiami, net jei prarasite įrenginį.",
|
||||
"currentUsageIs": "Dabartinis naudojimas – ",
|
||||
"@currentUsageIs": {
|
||||
"description": "This text is followed by storage usage",
|
||||
"examples": {
|
||||
"0": "Current usage is 1.2 GB"
|
||||
},
|
||||
"type": "text"
|
||||
},
|
||||
"faqs": "DUK",
|
||||
"freeTrialValidTill": "Nemokamas bandomasis laikotarpis galioja iki {endDate}",
|
||||
"validTill": "Galioja iki {endDate}",
|
||||
"subscription": "Prenumerata",
|
||||
"paymentDetails": "Mokėjimo duomenys",
|
||||
"manageFamily": "Tvarkyti šeimą",
|
||||
"renewSubscription": "Atnaujinti prenumeratą",
|
||||
"cancelSubscription": "Atsisakyti prenumeratos",
|
||||
"yesCancel": "Taip, atsisakyti",
|
||||
"failedToCancel": "Nepavyko atsisakyti",
|
||||
"twoMonthsFreeOnYearlyPlans": "2 mėnesiai nemokamai metiniuose planuose",
|
||||
@@ -310,11 +400,56 @@
|
||||
},
|
||||
"confirmPlanChange": "Patvirtinkite plano pakeitimą",
|
||||
"areYouSureYouWantToChangeYourPlan": "Ar tikrai norite keisti planą?",
|
||||
"youCannotDowngradeToThisPlan": "Negalite pakeisti į šį planą",
|
||||
"cancelOtherSubscription": "Pirmiausia atsisakykite esamos prenumeratos iš {paymentProvider}",
|
||||
"@cancelOtherSubscription": {
|
||||
"description": "The text to display when the user has an existing subscription from a different payment provider",
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"paymentProvider": {
|
||||
"example": "Apple",
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"optionalAsShortAsYouLike": "Nebūtina, trumpai, kaip jums patinka...",
|
||||
"send": "Siųsti",
|
||||
"googlePlayId": "„Google Play“ ID",
|
||||
"appleId": "„Apple ID“",
|
||||
"playstoreSubscription": "„PlayStore“ prenumerata",
|
||||
"subAlreadyLinkedErrMessage": "Jūsų {id} jau susietas su kita „Ente“ paskyra.\nJei norite naudoti savo {id} su šia paskyra, susisiekite su mūsų palaikymo komanda.",
|
||||
"visitWebToManage": "Aplankykite web.ente.io, kad tvarkytumėte savo prenumeratą",
|
||||
"paymentFailed": "Mokėjimas nepavyko",
|
||||
"paymentFailedTalkToProvider": "Kreipkitės į {providerName} palaikymo komandą, jei jums buvo nuskaičiuota.",
|
||||
"@paymentFailedTalkToProvider": {
|
||||
"description": "The text to display when the payment failed",
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"providerName": {
|
||||
"example": "AppStore|PlayStore",
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"continueOnFreeTrial": "Tęsti nemokame bandomajame laikotarpyje",
|
||||
"areYouSureYouWantToExit": "Ar tikrai norite išeiti?",
|
||||
"thankYou": "Dėkojame",
|
||||
"failedToVerifyPaymentStatus": "Nepavyko patvirtinti mokėjimo būsenos",
|
||||
"paymentFailedMessage": "Deja, jūsų mokėjimas nepavyko. Susisiekite su palaikymo komanda ir mes jums padėsime!",
|
||||
"leaveFamily": "Palikti šeimą",
|
||||
"areYouSureThatYouWantToLeaveTheFamily": "Ar tikrai norite palikti šeimos planą?",
|
||||
"leave": "Palikti",
|
||||
"rateTheApp": "Vertinti programą",
|
||||
"startBackup": "Pradėti kurti atsarginę kopiją",
|
||||
"existingUser": "Esamas naudotojas",
|
||||
"available": "Prieinama",
|
||||
"everywhere": "visur",
|
||||
"androidIosWebDesktop": "„Android“, „iOS“, internete ir darbalaukyje",
|
||||
"mobileWebDesktop": "Mobiliuosiuose, internete ir darbalaukyje",
|
||||
"newToEnte": "Naujas platformoje „Ente“",
|
||||
"pleaseLoginAgain": "Prisijunkite iš naujo.",
|
||||
"autoLogoutMessage": "Dėl techninio trikdžio buvote atjungti. Atsiprašome už nepatogumus.",
|
||||
"yourSubscriptionHasExpired": "Jūsų prenumerata baigėsi.",
|
||||
"storageLimitExceeded": "Viršyta saugyklos riba.",
|
||||
"upgrade": "Keisti planą",
|
||||
"raiseTicket": "Sukurti paraišką",
|
||||
@@ -327,6 +462,36 @@
|
||||
"type": "text"
|
||||
},
|
||||
"onDevice": "Įrenginyje",
|
||||
"@onEnte": {
|
||||
"description": "The text displayed above albums backed up to Ente",
|
||||
"type": "text"
|
||||
},
|
||||
"onEnte": "Saugykloje <branding>ente</branding>",
|
||||
"name": "Pavadinimą",
|
||||
"newest": "Naujausią",
|
||||
"lastUpdated": "Paskutinį kartą atnaujintą",
|
||||
"removeFromFavorite": "Šalinti iš mėgstamų",
|
||||
"addToEnte": "Pridėti į „Ente“",
|
||||
"addToAlbum": "Pridėti į albumą",
|
||||
"delete": "Ištrinti",
|
||||
"hide": "Slėpti",
|
||||
"share": "Bendrinti",
|
||||
"restoreToAlbum": "Atkurti į albumą",
|
||||
"moveItem": "{count, plural, one {Perkelti elementą} few {Perkelti elementus} many {Perkelti elemento} other {Perkelti elementų}}",
|
||||
"@moveItem": {
|
||||
"description": "Page title while moving one or more items to an album"
|
||||
},
|
||||
"shareAlbumHint": "Atidarykite albumą ir palieskite bendrinimo mygtuką viršuje dešinėje, kad bendrintumėte.",
|
||||
"setCover": "Nustatyti viršelį",
|
||||
"@setCover": {
|
||||
"description": "Text to set cover photo for an album"
|
||||
},
|
||||
"sortAlbumsBy": "Rikiuoti pagal",
|
||||
"sortNewestFirst": "Naujausią pirmiausiai",
|
||||
"sortOldestFirst": "Seniausią pirmiausiai",
|
||||
"rename": "Pervadinti",
|
||||
"leaveAlbum": "Palikti albumą",
|
||||
"photosAddedByYouWillBeRemovedFromTheAlbum": "Jūsų pridėtos nuotraukos bus pašalintos iš albumo",
|
||||
"noExifData": "Nėra EXIF duomenų",
|
||||
"thisImageHasNoExifData": "Šis vaizdas neturi Exif duomenų",
|
||||
"exif": "EXIF",
|
||||
@@ -334,7 +499,15 @@
|
||||
"close": "Uždaryti",
|
||||
"setAs": "Nustatyti kaip",
|
||||
"download": "Atsisiųsti",
|
||||
"pressAndHoldToPlayVideo": "Paspauskite ir palaikykite, kad paleistumėte vaizdo įrašą",
|
||||
"downloadFailed": "Atsisiuntimas nepavyko.",
|
||||
"deduplicateFiles": "Atdubliuoti failus",
|
||||
"reviewDeduplicateItems": "Peržiūrėkite ir ištrinkite elementus, kurie, jūsų manymu, yra dublikatai.",
|
||||
"unlock": "Atrakinti",
|
||||
"freeUpAmount": "Atlaisvinti {sizeInMBorGB}",
|
||||
"verificationFailedPleaseTryAgain": "Patvirtinimas nepavyko. Bandykite dar kartą.",
|
||||
"pleaseVerifyTheCodeYouHaveEntered": "Patvirtinkite įvestą kodą.",
|
||||
"yourVerificationCodeHasExpired": "Jūsų patvirtinimo kodo laikas nebegaliojantis.",
|
||||
"verifying": "Patvirtinama...",
|
||||
"loadingGallery": "Įkeliama galerija...",
|
||||
"syncing": "Sinchronizuojama...",
|
||||
@@ -380,6 +553,10 @@
|
||||
"addLocationButton": "Pridėti",
|
||||
"locationTagFeatureDescription": "Vietos žymė grupuoja visas nuotraukas, kurios buvo padarytos tam tikru spinduliu nuo nuotraukos",
|
||||
"centerPoint": "Vidurio taškas",
|
||||
"resetToDefault": "Atkurti numatytąsias reikšmes",
|
||||
"@resetToDefault": {
|
||||
"description": "Button text to reset cover photo to default"
|
||||
},
|
||||
"edit": "Redaguoti",
|
||||
"deleteLocation": "Ištrinti vietovę",
|
||||
"light": "Šviesi",
|
||||
@@ -403,6 +580,26 @@
|
||||
"@setLabel": {
|
||||
"description": "Label of confirm button to add a new custom radius to the radius selector of a location tag"
|
||||
},
|
||||
"androidBiometricHint": "Patvirtinkite tapatybę",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidCancelButton": "Atšaukti",
|
||||
"@androidCancelButton": {
|
||||
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on Android side. Maximum 30 characters."
|
||||
},
|
||||
"androidSignInTitle": "Privalomas tapatybės nustatymas",
|
||||
"@androidSignInTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user that they need to scan biometric to continue. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"goToSettings": "Eiti į nustatymus",
|
||||
"@goToSettings": {
|
||||
"description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters."
|
||||
},
|
||||
"iOSOkButton": "Gerai",
|
||||
"@iOSOkButton": {
|
||||
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters."
|
||||
},
|
||||
"map": "Žemėlapis",
|
||||
"@map": {
|
||||
"description": "Label for the map view"
|
||||
@@ -413,6 +610,8 @@
|
||||
"pinAlbum": "Prisegti albumą",
|
||||
"create": "Kurti",
|
||||
"viewAll": "Peržiūrėti viską",
|
||||
"deleteConfirmDialogBody": "Ši paskyra susieta su kitomis „Ente“ programomis, jei jas naudojate. Jūsų įkelti duomenys per visas „Ente“ programas bus planuojama ištrinti, o jūsų paskyra bus ištrinta negrįžtamai.",
|
||||
"viewAddOnButton": "Peržiūrėti priedus",
|
||||
"searchHint4": "Vietovė",
|
||||
"searchResultCount": "{count, plural, one{Rastas {count} rezultatas} few {Rasti {count} rezultatai} many {Rasta {count} rezultato} other{Rasta {count} rezultatų}}",
|
||||
"@searchResultCount": {
|
||||
@@ -439,7 +638,7 @@
|
||||
"selectALocation": "Pasirinkite vietovę",
|
||||
"selectALocationFirst": "Pirmiausia pasirinkite vietovę",
|
||||
"changeLocationOfSelectedItems": "Keisti pasirinktų elementų vietovę?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "Vietovės pakeitimai bus matomi tik platformoje „Ente“",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "Vietovės pakeitimai bus matomi tik per „Ente“",
|
||||
"cleanUncategorized": "Valyti nekategorizuotą",
|
||||
"cleanUncategorizedDescription": "Pašalinkite iš nekategorizuotą visus failus, esančius kituose albumuose",
|
||||
"waitingForVerification": "Laukiama patvirtinimo...",
|
||||
@@ -460,6 +659,8 @@
|
||||
"addAName": "Pridėti vardą",
|
||||
"findPeopleByName": "Greitai suraskite žmones pagal vardą",
|
||||
"addViewers": "{count, plural, one {Pridėti žiūrėtoją} few {Pridėti žiūrėtojus} many {Pridėti žiūrėtojo} other {Pridėti žiūrėtojų}}",
|
||||
"addCollaborators": "{count, plural, one {Pridėti bendradarbį} few {Pridėti bendradarbius} many {Pridėti bendradarbio} other {Pridėti bendradarbių}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Ilgai paspauskite el. paštą, kad patvirtintumėte visapusį šifravimą.",
|
||||
"developerSettingsWarning": "Ar tikrai norite modifikuoti kūrėjo nustatymus?",
|
||||
"developerSettings": "Kūrėjo nustatymai",
|
||||
"serverEndpoint": "Serverio galutinis taškas",
|
||||
@@ -470,13 +671,16 @@
|
||||
"createCollaborativeLink": "Kurti bendradarbiavimo nuorodą",
|
||||
"search": "Ieškoti",
|
||||
"enterPersonName": "Įveskite asmens vardą",
|
||||
"removePersonLabel": "Pašalinti asmens žymą",
|
||||
"removePersonLabel": "Šalinti asmens žymą",
|
||||
"autoPairDesc": "Automatinis susiejimas veikia tik su įrenginiais, kurie palaiko „Chromecast“.",
|
||||
"manualPairDesc": "Susieti su PIN kodu veikia bet kuriame ekrane, kuriame norite peržiūrėti albumą.",
|
||||
"connectToDevice": "Prijungti prie įrenginio",
|
||||
"autoCastDialogBody": "Čia matysite pasiekiamus perdavimo įrenginius.",
|
||||
"noDeviceFound": "Įrenginys nerastas",
|
||||
"stopCastingTitle": "Stabdyti perdavimą",
|
||||
"stopCastingBody": "Ar norite sustabdyti perdavimą?",
|
||||
"castIPMismatchTitle": "Nepavyko perduoti albumo",
|
||||
"castIPMismatchBody": "Įsitikinkite, kad esate tame pačiame tinkle kaip ir televizorius.",
|
||||
"pairingComplete": "Susiejimas baigtas",
|
||||
"savingEdits": "Išsaugomi redagavimai...",
|
||||
"autoPair": "Automatiškai susieti",
|
||||
@@ -507,13 +711,13 @@
|
||||
"enabled": "Įjungta",
|
||||
"moreDetails": "Daugiau išsamios informacijos",
|
||||
"enableMLIndexingDesc": "„Ente“ palaiko įrenginyje mašininį mokymąsi, skirtą veidų atpažinimui, magiškai paieškai ir kitoms išplėstinėms paieškos funkcijoms",
|
||||
"magicSearchHint": "Magiška paieška leidžia ieškoti nuotraukų pagal jų turinį, pvz., „\"gėlė“, „raudonas automobilis“, „tapatybės dokumentai“",
|
||||
"magicSearchHint": "Magiška paieška leidžia ieškoti nuotraukų pagal jų turinį, pvz., „gėlė“, „raudonas automobilis“, „tapatybės dokumentai“",
|
||||
"panorama": "Panorama",
|
||||
"reenterPassword": "Įveskite slaptažodį iš naujo",
|
||||
"reenterPin": "Įveskite PIN iš naujo",
|
||||
"deviceLock": "Įrenginio užraktas",
|
||||
"pinLock": "PIN užrakinimas",
|
||||
"next": "Sekantis",
|
||||
"next": "Toliau",
|
||||
"setNewPassword": "Nustatykite naują slaptažodį",
|
||||
"enterPin": "Įveskite PIN",
|
||||
"setNewPin": "Nustatykite naują PIN",
|
||||
@@ -531,7 +735,7 @@
|
||||
"passwordStrengthInfo": "Slaptažodžio stiprumas apskaičiuojamas atsižvelgiant į slaptažodžio ilgį, naudotus simbolius ir į tai, ar slaptažodis patenka į 10 000 dažniausiai naudojamų slaptažodžių.",
|
||||
"noQuickLinksSelected": "Nėra pasirinktų sparčiųjų nuorodų",
|
||||
"pleaseSelectQuickLinksToRemove": "Pasirinkite sparčiąsias nuorodas, kad pašalintumėte",
|
||||
"removePublicLinks": "Pašalinti viešąsias nuorodas",
|
||||
"removePublicLinks": "Šalinti viešąsias nuorodas",
|
||||
"thisWillRemovePublicLinksOfAllSelectedQuickLinks": "Tai pašalins visų pasirinktų sparčiųjų nuorodų viešąsias nuorodas.",
|
||||
"guestView": "Svečio peržiūra",
|
||||
"guestViewEnablePreSteps": "Kad įjungtumėte svečio peržiūrą, sistemos nustatymuose nustatykite įrenginio prieigos kodą arba ekrano užraktą.",
|
||||
|
||||
@@ -1031,27 +1031,27 @@
|
||||
"searchPeopleEmptySection": "Convide pessoas e você verá todas as fotos compartilhadas por elas aqui",
|
||||
"searchAlbumsEmptySection": "Álbuns",
|
||||
"searchFileTypesAndNamesEmptySection": "Tipos de arquivo e nomes",
|
||||
"searchCaptionEmptySection": "Adicione descrições como \"#trip\" nas informações das fotos para encontrá-las aqui rapidamente",
|
||||
"searchCaptionEmptySection": "Adicione marcações como \"#viagem\" nas informações das fotos para encontrá-las aqui com facilidade",
|
||||
"language": "Idioma",
|
||||
"selectLanguage": "Selecionar Idioma",
|
||||
"locationName": "Nome do Local",
|
||||
"addLocation": "Adicionar local",
|
||||
"selectLanguage": "Selecionar idioma",
|
||||
"locationName": "Nome da localização",
|
||||
"addLocation": "Adicionar localização",
|
||||
"groupNearbyPhotos": "Agrupar fotos próximas",
|
||||
"kiloMeterUnit": "km",
|
||||
"addLocationButton": "Adicionar",
|
||||
"radius": "Raio",
|
||||
"locationTagFeatureDescription": "Uma tag em grupo de todas as fotos que foram tiradas dentro de algum raio de uma foto",
|
||||
"galleryMemoryLimitInfo": "Até 1000 memórias mostradas na galeria",
|
||||
"locationTagFeatureDescription": "Uma etiqueta de localização agrupa todas as fotos fotografadas em algum raio de uma foto",
|
||||
"galleryMemoryLimitInfo": "Até 1.000 memórias exibidas na galeria",
|
||||
"save": "Salvar",
|
||||
"centerPoint": "Ponto central",
|
||||
"pickCenterPoint": "Escolha o ponto central",
|
||||
"useSelectedPhoto": "Utilizar foto selecionada",
|
||||
"useSelectedPhoto": "Usar foto selecionada",
|
||||
"resetToDefault": "Redefinir para o padrão",
|
||||
"@resetToDefault": {
|
||||
"description": "Button text to reset cover photo to default"
|
||||
},
|
||||
"edit": "Editar",
|
||||
"deleteLocation": "Excluir Local",
|
||||
"deleteLocation": "Excluir localização",
|
||||
"rotateLeft": "Girar para a esquerda",
|
||||
"flip": "Inverter",
|
||||
"rotateRight": "Girar para a direita",
|
||||
@@ -1062,7 +1062,7 @@
|
||||
"doYouWantToDiscardTheEditsYouHaveMade": "Você quer descartar as edições que você fez?",
|
||||
"saving": "Salvando...",
|
||||
"editsSaved": "Edições salvas",
|
||||
"oopsCouldNotSaveEdits": "Ops, não foi possível salvar edições",
|
||||
"oopsCouldNotSaveEdits": "Opa! Não foi possível salvar as edições",
|
||||
"distanceInKMUnit": "km",
|
||||
"@distanceInKMUnit": {
|
||||
"description": "Unit for distance in km"
|
||||
@@ -1070,7 +1070,7 @@
|
||||
"dayToday": "Hoje",
|
||||
"dayYesterday": "Ontem",
|
||||
"storage": "Armazenamento",
|
||||
"usedSpace": "Espaço em uso",
|
||||
"usedSpace": "Espaço usado",
|
||||
"storageBreakupFamily": "Família",
|
||||
"storageBreakupYou": "Você",
|
||||
"@storageBreakupYou": {
|
||||
@@ -1084,14 +1084,14 @@
|
||||
"appVersion": "Versão: {versionValue}",
|
||||
"verifyIDLabel": "Verificar",
|
||||
"fileInfoAddDescHint": "Adicionar descrição...",
|
||||
"editLocationTagTitle": "Editar local",
|
||||
"editLocationTagTitle": "Editar localização",
|
||||
"setLabel": "Definir",
|
||||
"@setLabel": {
|
||||
"description": "Label of confirm button to add a new custom radius to the radius selector of a location tag"
|
||||
},
|
||||
"setRadius": "Definir raio",
|
||||
"familyPlanPortalTitle": "Família",
|
||||
"familyPlanOverview": "Adicione 5 membros da família ao seu plano existente sem pagar a mais.\n\nCada membro recebe seu próprio espaço privado, e nenhum membro pode ver os arquivos uns dos outros a menos que sejam compartilhados.\n\nPlanos de família estão disponíveis para os clientes que têm uma assinatura do Ente paga.\n\nAssine agora para começar!",
|
||||
"familyPlanOverview": "Adicione 5 familiares para seu plano existente sem pagar nenhum custo adicional.\n\nCada membro ganha seu espaço privado, significando que eles não podem ver os arquivos dos outros a menos que eles sejam compartilhados.\n\nOs planos familiares estão disponíveis para clientes que já tem uma assinatura paga do Ente.\n\nAssine agora para iniciar!",
|
||||
"androidBiometricHint": "Verificar identidade",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
@@ -1116,27 +1116,27 @@
|
||||
"@androidBiometricRequiredTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user has not set up biometric authentication on their device. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidDeviceCredentialsRequiredTitle": "Credenciais do dispositivo necessárias",
|
||||
"androidDeviceCredentialsRequiredTitle": "Credenciais necessários",
|
||||
"@androidDeviceCredentialsRequiredTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user has not set up credentials authentication on their device. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidDeviceCredentialsSetupDescription": "Credenciais do dispositivo necessárias",
|
||||
"androidDeviceCredentialsSetupDescription": "Credenciais necessários",
|
||||
"@androidDeviceCredentialsSetupDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure device credentials on their device. It shows in a dialog on Android side."
|
||||
},
|
||||
"goToSettings": "Ir para Configurações",
|
||||
"goToSettings": "Ir às opções",
|
||||
"@goToSettings": {
|
||||
"description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters."
|
||||
},
|
||||
"androidGoToSettingsDescription": "A autenticação biométrica não está configurada no seu dispositivo. Vá em 'Configurações > Segurança' para adicionar autenticação biométrica.",
|
||||
"androidGoToSettingsDescription": "A autenticação biométrica não está definida no dispositivo. Vá em 'Opções > Segurança' para adicionar a autenticação biométrica.",
|
||||
"@androidGoToSettingsDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure biometric on their device. It shows in a dialog on Android side."
|
||||
},
|
||||
"iOSLockOut": "A Autenticação Biométrica está desativada. Por favor, bloqueie e desbloqueie sua tela para ativá-la.",
|
||||
"iOSLockOut": "A autenticação biométrica está desativada. Bloqueie e desbloqueie sua tela para ativá-la.",
|
||||
"@iOSLockOut": {
|
||||
"description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side."
|
||||
},
|
||||
"iOSGoToSettingsDescription": "A autenticação biométrica não está configurada no seu dispositivo. Por favor, ative o Touch ID ou o Face ID no seu telefone.",
|
||||
"iOSGoToSettingsDescription": "A autenticação biométrica não está definida no dispositivo. Ative o Touch ID ou Face ID no dispositivo.",
|
||||
"@iOSGoToSettingsDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure Biometrics for their device. It shows in a dialog on iOS side."
|
||||
},
|
||||
@@ -1145,22 +1145,22 @@
|
||||
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters."
|
||||
},
|
||||
"openstreetmapContributors": "Contribuidores do OpenStreetMap",
|
||||
"hostedAtOsmFrance": "Hospedado na OSM France",
|
||||
"hostedAtOsmFrance": "Hospedado em OSM France",
|
||||
"map": "Mapa",
|
||||
"@map": {
|
||||
"description": "Label for the map view"
|
||||
},
|
||||
"maps": "Mapas",
|
||||
"enableMaps": "Habilitar Mapa",
|
||||
"enableMapsDesc": "Isto mostrará suas fotos em um mapa do mundo.\n\nEste mapa é hospedado pelo OpenStreetMap, e os exatos locais de suas fotos nunca são compartilhados.\n\nVocê pode desativar esse recurso a qualquer momento nas Configurações.",
|
||||
"enableMaps": "Ativar mapas",
|
||||
"enableMapsDesc": "Isso exibirá suas fotos em um mapa mundial.\n\nEste mapa é hospedado por Open Street Map, e as exatas localizações das fotos nunca serão compartilhadas.\n\nVocê pode desativar esta função a qualquer momento em Opções.",
|
||||
"quickLinks": "Links rápidos",
|
||||
"selectItemsToAdd": "Selecionar itens para adicionar",
|
||||
"addSelected": "Adicionar selecionado",
|
||||
"addFromDevice": "Adicionar a partir do dispositivo",
|
||||
"addFromDevice": "Adicionar do dispositivo",
|
||||
"addPhotos": "Adicionar fotos",
|
||||
"noPhotosFoundHere": "Nenhuma foto encontrada aqui",
|
||||
"zoomOutToSeePhotos": "Diminuir o zoom para ver fotos",
|
||||
"noImagesWithLocation": "Nenhuma imagem com local",
|
||||
"zoomOutToSeePhotos": "Reduzir ampliação para ver as fotos",
|
||||
"noImagesWithLocation": "Nenhuma imagem com localização",
|
||||
"unpinAlbum": "Desafixar álbum",
|
||||
"pinAlbum": "Fixar álbum",
|
||||
"create": "Criar",
|
||||
@@ -1170,19 +1170,19 @@
|
||||
"sharedWithYou": "Compartilhado com você",
|
||||
"sharedByYou": "Compartilhado por você",
|
||||
"inviteYourFriendsToEnte": "Convide seus amigos ao Ente",
|
||||
"failedToDownloadVideo": "Falha ao fazer download do vídeo",
|
||||
"failedToDownloadVideo": "Falhou ao baixar vídeo",
|
||||
"hiding": "Ocultando...",
|
||||
"unhiding": "Reexibindo...",
|
||||
"successfullyHid": "Ocultado com sucesso",
|
||||
"successfullyUnhid": "Reexibido com sucesso",
|
||||
"crashReporting": "Relatório de falhas",
|
||||
"successfullyUnhid": "Desocultado com sucesso",
|
||||
"crashReporting": "Relatório de erros",
|
||||
"resumableUploads": "Envios retomáveis",
|
||||
"addToHiddenAlbum": "Adicionar a álbum oculto",
|
||||
"moveToHiddenAlbum": "Mover para álbum oculto",
|
||||
"addToHiddenAlbum": "Adicionar ao álbum oculto",
|
||||
"moveToHiddenAlbum": "Mover ao álbum oculto",
|
||||
"fileTypes": "Tipos de arquivo",
|
||||
"deleteConfirmDialogBody": "Esta conta está vinculada a outros aplicativos Ente, se você usar algum. Seus dados enviados, em todos os aplicativos Ente, serão agendados para exclusão, e sua conta será excluída permanentemente.",
|
||||
"hearUsWhereTitle": "Como você ouviu sobre o Ente? (opcional)",
|
||||
"hearUsExplanation": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!",
|
||||
"deleteConfirmDialogBody": "Esta conta está vinculada aos outros aplicativos do Ente, se você usar algum. Seus dados baixados, entre todos os aplicativos do Ente, serão programados para exclusão, e sua conta será permanentemente excluída.",
|
||||
"hearUsWhereTitle": "Como você soube do Ente? (opcional)",
|
||||
"hearUsExplanation": "Não rastreamos instalações de aplicativo. Seria útil se você contasse onde nos encontrou!",
|
||||
"viewAddOnButton": "Ver complementos",
|
||||
"addOns": "Complementos",
|
||||
"addOnPageSubtitle": "Detalhes dos complementos",
|
||||
@@ -1297,8 +1297,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"enable": "Habilitar",
|
||||
"enabled": "Habilitado",
|
||||
"enable": "Ativar",
|
||||
"enabled": "Ativado",
|
||||
"moreDetails": "Mais detalhes",
|
||||
"enableMLIndexingDesc": "Ente suporta aprendizado de máquina no dispositivo para reconhecimento facial, busca mágica e outros recursos avançados de busca",
|
||||
"magicSearchHint": "A busca mágica permite pesquisar fotos por seu conteúdo, por exemplo, 'carro', 'carro vermelho', 'Ferrari'",
|
||||
|
||||
@@ -286,13 +286,13 @@
|
||||
"details": "Подробиці",
|
||||
"claimMore": "Отримайте більше!",
|
||||
"theyAlsoGetXGb": "Вони також отримують {storageAmountInGB} ГБ",
|
||||
"freeStorageOnReferralSuccess": "{storageAmountInGB} ГБ щоразу, коли хтось підписується на платний тариф і застосовує ваш код",
|
||||
"freeStorageOnReferralSuccess": "{storageAmountInGB} ГБ щоразу, коли хтось оформлює передплату і застосовує ваш код",
|
||||
"shareTextReferralCode": "Реферальний код Ente: {referralCode} \n\nЗастосуйте його в «Налаштування» → «Загальні» → «Реферали», щоб отримати {referralStorageInGB} ГБ безплатно після переходу на платний тариф\n\nhttps://ente.io",
|
||||
"claimFreeStorage": "Отримайте безплатне сховище",
|
||||
"inviteYourFriends": "Запросити своїх друзів",
|
||||
"failedToFetchReferralDetails": "Не вдається отримати відомості про реферала. Спробуйте ще раз пізніше.",
|
||||
"referralStep1": "1. Дайте цей код друзям",
|
||||
"referralStep2": "2. Вони підписуються на платний план",
|
||||
"referralStep2": "2. Вони оформлюють передплату",
|
||||
"referralStep3": "3. Ви обоє отримуєте {storageInGB} ГБ* безплатно",
|
||||
"referralsAreCurrentlyPaused": "Реферали зараз призупинені",
|
||||
"youCanAtMaxDoubleYourStorage": "* Ви можете максимально подвоїти своє сховище",
|
||||
@@ -327,8 +327,8 @@
|
||||
"removingFromFavorites": "Видалення з обраного...",
|
||||
"sorryCouldNotAddToFavorites": "Неможливо додати до обраного!",
|
||||
"sorryCouldNotRemoveFromFavorites": "Не вдалося видалити з обраного!",
|
||||
"subscribeToEnableSharing": "Вам потрібна активна платна підписка, щоб увімкнути спільне поширення.",
|
||||
"subscribe": "Підписатися",
|
||||
"subscribeToEnableSharing": "Вам потрібна активна передплата, щоб увімкнути спільне поширення.",
|
||||
"subscribe": "Передплачувати",
|
||||
"canOnlyRemoveFilesOwnedByYou": "Ви можете видалити лише файли, що належать вам",
|
||||
"deleteSharedAlbum": "Видалити спільний альбом?",
|
||||
"deleteAlbum": "Видалити альбом",
|
||||
@@ -487,7 +487,7 @@
|
||||
"checking": "Перевірка...",
|
||||
"youAreOnTheLatestVersion": "Ви використовуєте останню версію",
|
||||
"account": "Обліковий запис",
|
||||
"manageSubscription": "Керування підпискою",
|
||||
"manageSubscription": "Керування передплатою",
|
||||
"authToChangeYourEmail": "Авторизуйтесь, щоб змінити поштову адресу",
|
||||
"changePassword": "Змінити пароль",
|
||||
"authToChangeYourPassword": "Авторизуйтесь, щоб змінити пароль",
|
||||
@@ -601,18 +601,18 @@
|
||||
"type": "text"
|
||||
},
|
||||
"faqs": "ЧаПи",
|
||||
"renewsOn": "Підписка поновиться {endDate}",
|
||||
"renewsOn": "Передплата поновиться {endDate}",
|
||||
"freeTrialValidTill": "Безплатна пробна версія діє до {endDate}",
|
||||
"validTill": "Діє до {endDate}",
|
||||
"addOnValidTill": "Ваше доповнення {storageAmount} діє до {endDate}",
|
||||
"playStoreFreeTrialValidTill": "Безплатна пробна версія діє до {endDate}.\nПісля цього ви можете обрати платний план.",
|
||||
"subWillBeCancelledOn": "Вашу підписку буде скасовано {endDate}",
|
||||
"subscription": "Підписка",
|
||||
"subWillBeCancelledOn": "Вашу передплату буде скасовано {endDate}",
|
||||
"subscription": "Передплата",
|
||||
"paymentDetails": "Деталі платежу",
|
||||
"manageFamily": "Керування сім'єю",
|
||||
"contactToManageSubscription": "Зв'яжіться з нами за адресою support@ente.io для управління вашою підпискою {provider}.",
|
||||
"renewSubscription": "Поновити підписку",
|
||||
"cancelSubscription": "Скасувати підписку",
|
||||
"contactToManageSubscription": "Зв'яжіться з нами за адресою support@ente.io для управління вашою передплатою {provider}.",
|
||||
"renewSubscription": "Поновити передплату",
|
||||
"cancelSubscription": "Скасувати передплату",
|
||||
"areYouSureYouWantToRenew": "Ви впевнені, що хочете поновити?",
|
||||
"yesRenew": "Так, поновити",
|
||||
"areYouSureYouWantToCancel": "Ви дійсно хочете скасувати?",
|
||||
@@ -633,7 +633,7 @@
|
||||
"confirmPlanChange": "Підтвердити зміну плану",
|
||||
"areYouSureYouWantToChangeYourPlan": "Ви впевнені, що хочете змінити свій план?",
|
||||
"youCannotDowngradeToThisPlan": "Ви не можете перейти до цього плану",
|
||||
"cancelOtherSubscription": "Спочатку скасуйте вашу підписку від {paymentProvider}",
|
||||
"cancelOtherSubscription": "Спочатку скасуйте вашу передплату від {paymentProvider}",
|
||||
"@cancelOtherSubscription": {
|
||||
"description": "The text to display when the user has an existing subscription from a different payment provider",
|
||||
"type": "text",
|
||||
@@ -646,19 +646,19 @@
|
||||
},
|
||||
"optionalAsShortAsYouLike": "Необов'язково, так коротко, як ви хочете...",
|
||||
"send": "Надіслати",
|
||||
"askCancelReason": "Підписку було скасовано. Ви хотіли б поділитися причиною?",
|
||||
"thankYouForSubscribing": "Спасибі за підписку!",
|
||||
"askCancelReason": "Передплату було скасовано. Ви хотіли б поділитися причиною?",
|
||||
"thankYouForSubscribing": "Спасибі за передплату!",
|
||||
"yourPurchaseWasSuccessful": "Ваша покупка пройшла успішно",
|
||||
"yourPlanWasSuccessfullyUpgraded": "Ваш план успішно покращено",
|
||||
"yourPlanWasSuccessfullyDowngraded": "Ваш план був успішно знижено",
|
||||
"yourSubscriptionWasUpdatedSuccessfully": "Вашу підписку успішно оновлено",
|
||||
"yourSubscriptionWasUpdatedSuccessfully": "Вашу передплату успішно оновлено",
|
||||
"googlePlayId": "Google Play ID",
|
||||
"appleId": "Apple ID",
|
||||
"playstoreSubscription": "Підписка Play Store",
|
||||
"appstoreSubscription": "Підписка App Store",
|
||||
"playstoreSubscription": "Передплата Play Store",
|
||||
"appstoreSubscription": "Передплата App Store",
|
||||
"subAlreadyLinkedErrMessage": "Ваш {id} вже пов'язаний з іншим обліковим записом Ente.\nЯкщо ви хочете використовувати свій {id} з цим обліковим записом, зверніться до нашої служби підтримки",
|
||||
"visitWebToManage": "Відвідайте web.ente.io, щоб керувати підпискою",
|
||||
"couldNotUpdateSubscription": "Не вдалося оновити підписку",
|
||||
"visitWebToManage": "Відвідайте web.ente.io, щоб керувати передплатою",
|
||||
"couldNotUpdateSubscription": "Не вдалося оновити передплату",
|
||||
"pleaseContactSupportAndWeWillBeHappyToHelp": "Зв'яжіться з support@ente.io і ми будемо раді допомогти!",
|
||||
"paymentFailed": "Не вдалося оплатити",
|
||||
"paymentFailedTalkToProvider": "Зверніться до {providerName}, якщо було знято платіж",
|
||||
@@ -679,7 +679,7 @@
|
||||
"pleaseWaitForSometimeBeforeRetrying": "Зачекайте деякий час перед повторною спробою",
|
||||
"paymentFailedMessage": "На жаль, ваш платіж не вдався. Зв'яжіться зі службою підтримки і ми вам допоможемо!",
|
||||
"youAreOnAFamilyPlan": "Ви на сімейному плані!",
|
||||
"contactFamilyAdmin": "Зв'яжіться з <green>{familyAdminEmail}</green> для керування вашою підпискою",
|
||||
"contactFamilyAdmin": "Зв'яжіться з <green>{familyAdminEmail}</green> для керування вашою передплатою",
|
||||
"leaveFamily": "Покинути сім'ю",
|
||||
"areYouSureThatYouWantToLeaveTheFamily": "Ви впевнені, що хочете залишити сімейний план?",
|
||||
"leave": "Покинути",
|
||||
@@ -704,7 +704,7 @@
|
||||
"newToEnte": "Уперше на Ente",
|
||||
"pleaseLoginAgain": "Увійдіть знову",
|
||||
"autoLogoutMessage": "Через технічні збої ви вийшли з системи. Перепрошуємо за незручності.",
|
||||
"yourSubscriptionHasExpired": "Термін дії вашої підписки скінчився",
|
||||
"yourSubscriptionHasExpired": "Термін дії вашої передплати скінчився",
|
||||
"storageLimitExceeded": "Перевищено ліміт сховища",
|
||||
"upgrade": "Покращити",
|
||||
"raiseTicket": "Подати заявку",
|
||||
@@ -923,7 +923,7 @@
|
||||
"@freeUpSpaceSaving": {
|
||||
"description": "Text to tell user how much space they can free up by deleting items from the device"
|
||||
},
|
||||
"freeUpAccessPostDelete": "Ви все ще можете отримати доступ до {count, plural, one {нього} other {них}} в Ente, доки у вас активна підписка",
|
||||
"freeUpAccessPostDelete": "Ви все ще можете отримати доступ до {count, plural, one {нього} other {них}} в Ente, доки у вас активна передплата",
|
||||
"@freeUpAccessPostDelete": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -956,6 +956,7 @@
|
||||
"encryptingBackup": "Шифруємо резервну копію...",
|
||||
"syncStopped": "Синхронізацію зупинено",
|
||||
"syncProgress": "{completed} / {total} спогадів збережено",
|
||||
"uploadingMultipleMemories": "Збереження {count} спогадів...",
|
||||
"uploadingSingleMemory": "Зберігаємо 1 спогад...",
|
||||
"@syncProgress": {
|
||||
"description": "Text to tell user how many memories have been preserved",
|
||||
@@ -1011,7 +1012,7 @@
|
||||
"dismiss": "Відхилити",
|
||||
"didYouKnow": "Чи знали ви?",
|
||||
"loadingMessage": "Завантажуємо ваші фотографії...",
|
||||
"loadMessage1": "Ви можете поділитися своєю підпискою з родиною",
|
||||
"loadMessage1": "Ви можете поділитися своєю передплатою з родиною",
|
||||
"loadMessage2": "На цей час ми зберегли понад 30 мільйонів спогадів",
|
||||
"loadMessage3": "Ми зберігаємо 3 копії ваших даних, одну в підземному бункері",
|
||||
"loadMessage4": "Всі наші застосунки мають відкритий код",
|
||||
|
||||
@@ -135,11 +135,21 @@ class MultiPartUploader {
|
||||
|
||||
if (multipartInfo.status != MultipartStatus.completed) {
|
||||
// complete the multipart upload
|
||||
await _completeMultipartUpload(
|
||||
multipartInfo.urls.objectKey,
|
||||
etags,
|
||||
multipartInfo.urls.completeURL,
|
||||
);
|
||||
try {
|
||||
await _completeMultipartUpload(
|
||||
multipartInfo.urls.objectKey,
|
||||
etags,
|
||||
multipartInfo.urls.completeURL,
|
||||
);
|
||||
} on DioError catch (e) {
|
||||
if (e.response?.statusCode == 404) {
|
||||
_logger.severe(
|
||||
"Multipart upload not found for key ${multipartInfo.urls.objectKey}",
|
||||
);
|
||||
await _db.deleteMultipartTrack(localId);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
return multipartInfo.urls.objectKey;
|
||||
@@ -263,7 +273,7 @@ class MultiPartUploader {
|
||||
MultipartStatus.completed,
|
||||
);
|
||||
} catch (e) {
|
||||
Logger("MultipartUpload").severe(e);
|
||||
Logger("MultipartUpload").severe("upload failed for key $objectKey}", e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { MenuItemDivider, MenuItemGroup } from "@/base/components/Menu";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import { errorDialogAttributes } from "@/base/components/utils/dialog";
|
||||
import log from "@/base/log";
|
||||
@@ -283,7 +283,7 @@ const ManagePasskeyDrawer: React.FC<ManagePasskeyDrawerProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<EnteDrawer anchor="right" {...{ open, onClose }}>
|
||||
<SidebarDrawer anchor="right" {...{ open, onClose }}>
|
||||
{token && passkey && (
|
||||
<Stack spacing={"4px"} py={"12px"}>
|
||||
<Titlebar
|
||||
@@ -312,7 +312,7 @@ const ManagePasskeyDrawer: React.FC<ManagePasskeyDrawerProps> = ({
|
||||
</MenuItemGroup>
|
||||
</Stack>
|
||||
)}
|
||||
</EnteDrawer>
|
||||
</SidebarDrawer>
|
||||
|
||||
{token && passkey && (
|
||||
<RenamePasskeyDialog
|
||||
|
||||
@@ -32,14 +32,7 @@ import { ThemeProvider } from "@mui/material/styles";
|
||||
import { t } from "i18next";
|
||||
import type { AppProps } from "next/app";
|
||||
import { useRouter } from "next/router";
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import LoadingBar, { type LoadingBarRef } from "react-top-loading-bar";
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
import "../../public/css/global.css";
|
||||
|
||||
@@ -47,8 +40,6 @@ import "../../public/css/global.css";
|
||||
* Properties available via {@link AppContext} to the Auth app's React tree.
|
||||
*/
|
||||
type AppContextT = AccountsContextT & {
|
||||
startLoading: () => void;
|
||||
finishLoading: () => void;
|
||||
themeColor: THEME_COLOR;
|
||||
setThemeColor: (themeColor: THEME_COLOR) => void;
|
||||
somethingWentWrong: () => void;
|
||||
@@ -68,8 +59,6 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
typeof window !== "undefined" && !window.navigator.onLine,
|
||||
);
|
||||
const [showNavbar, setShowNavBar] = useState(false);
|
||||
const isLoadingBarRunning = useRef<boolean>(false);
|
||||
const loadingBar = useRef<LoadingBarRef>(null);
|
||||
|
||||
const { showMiniDialog, miniDialogProps } = useAttributedMiniDialog();
|
||||
const [themeColor, setThemeColor] = useLocalState(
|
||||
@@ -113,18 +102,6 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
|
||||
const showNavBar = (show: boolean) => setShowNavBar(show);
|
||||
|
||||
const startLoading = () => {
|
||||
!isLoadingBarRunning.current && loadingBar.current?.continuousStart();
|
||||
isLoadingBarRunning.current = true;
|
||||
};
|
||||
|
||||
const finishLoading = () => {
|
||||
setTimeout(() => {
|
||||
isLoadingBarRunning.current && loadingBar.current?.complete();
|
||||
isLoadingBarRunning.current = false;
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const somethingWentWrong = () =>
|
||||
showMiniDialog(genericErrorDialogAttributes());
|
||||
|
||||
@@ -136,8 +113,6 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
logout,
|
||||
showNavBar,
|
||||
showMiniDialog,
|
||||
startLoading,
|
||||
finishLoading,
|
||||
themeColor,
|
||||
setThemeColor,
|
||||
somethingWentWrong,
|
||||
@@ -156,8 +131,6 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
{isI18nReady && offline && t("OFFLINE_MSG")}
|
||||
</MessageContainer>
|
||||
|
||||
<LoadingBar color="#51cd7c" ref={loadingBar} />
|
||||
|
||||
<AttributedMiniDialog {...miniDialogProps} />
|
||||
|
||||
<AppContext.Provider value={appContext}>
|
||||
|
||||
@@ -24,12 +24,10 @@
|
||||
"ml-matrix": "^6.11",
|
||||
"p-debounce": "^4.0.0",
|
||||
"photoswipe": "file:./thirdparty/photoswipe",
|
||||
"piexifjs": "^1.0.6",
|
||||
"pure-react-carousel": "^1.30.1",
|
||||
"react-dropzone": "^14.2",
|
||||
"react-otp-input": "^2.3.1",
|
||||
"react-select": "^5.8.0",
|
||||
"react-top-loading-bar": "^2.0.1",
|
||||
"react-top-loading-bar": "^2.3.1",
|
||||
"react-virtualized-auto-sizer": "^1.0",
|
||||
"react-window": "^1.8.10",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
|
||||
@@ -37,7 +37,8 @@ import LinkIcon from "@mui/icons-material/Link";
|
||||
import LogoutIcon from "@mui/icons-material/Logout";
|
||||
import MoreHoriz from "@mui/icons-material/MoreHoriz";
|
||||
import PeopleIcon from "@mui/icons-material/People";
|
||||
import PushPinOutlined from "@mui/icons-material/PushPinOutlined";
|
||||
import PushPinIcon from "@mui/icons-material/PushPin";
|
||||
import PushPinOutlinedIcon from "@mui/icons-material/PushPinOutlined";
|
||||
import SortIcon from "@mui/icons-material/Sort";
|
||||
import TvIcon from "@mui/icons-material/Tv";
|
||||
import Unarchive from "@mui/icons-material/Unarchive";
|
||||
@@ -45,7 +46,6 @@ import VisibilityOffOutlined from "@mui/icons-material/VisibilityOffOutlined";
|
||||
import VisibilityOutlined from "@mui/icons-material/VisibilityOutlined";
|
||||
import { Box, IconButton, Stack, Tooltip } from "@mui/material";
|
||||
import { SetCollectionNamerAttributes } from "components/Collections/CollectionNamer";
|
||||
import { UnPinIcon } from "components/icons/UnPinIcon";
|
||||
import { t } from "i18next";
|
||||
import { GalleryContext } from "pages/gallery";
|
||||
import React, { useCallback, useContext, useRef } from "react";
|
||||
@@ -141,7 +141,7 @@ const CollectionOptions: React.FC<CollectionOptionsProps> = ({
|
||||
setFilesDownloadProgressAttributesCreator,
|
||||
isActiveCollectionDownloadInProgress,
|
||||
}) => {
|
||||
const { startLoading, finishLoading, setDialogMessage } =
|
||||
const { showLoadingBar, hideLoadingBar, setDialogMessage } =
|
||||
useContext(AppContext);
|
||||
const { syncWithRemote } = useContext(GalleryContext);
|
||||
const overFlowMenuIconRef = useRef<SVGSVGElement>(null);
|
||||
@@ -169,19 +169,19 @@ const CollectionOptions: React.FC<CollectionOptionsProps> = ({
|
||||
const wrap = useCallback(
|
||||
(f: () => Promise<void>) => {
|
||||
const wrapped = async () => {
|
||||
startLoading();
|
||||
showLoadingBar();
|
||||
try {
|
||||
await f();
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
} finally {
|
||||
void syncWithRemote(false, true);
|
||||
finishLoading();
|
||||
hideLoadingBar();
|
||||
}
|
||||
};
|
||||
return (): void => void wrapped();
|
||||
},
|
||||
[handleError, syncWithRemote, startLoading, finishLoading],
|
||||
[handleError, syncWithRemote, showLoadingBar, hideLoadingBar],
|
||||
);
|
||||
|
||||
const showRenameCollectionModal = () => {
|
||||
@@ -654,14 +654,14 @@ const AlbumCollectionOptions: React.FC<AlbumCollectionOptionsProps> = ({
|
||||
{isPinned ? (
|
||||
<OverflowMenuOption
|
||||
onClick={onUnpinClick}
|
||||
startIcon={<UnPinIcon />}
|
||||
startIcon={<PushPinOutlinedIcon />}
|
||||
>
|
||||
{t("unpin_album")}
|
||||
</OverflowMenuOption>
|
||||
) : (
|
||||
<OverflowMenuOption
|
||||
onClick={onPinClick}
|
||||
startIcon={<PushPinOutlined />}
|
||||
startIcon={<PushPinIcon />}
|
||||
>
|
||||
{t("pin_album")}
|
||||
</OverflowMenuOption>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import { COLLECTION_ROLE, type Collection } from "@/media/collection";
|
||||
import { DialogProps, Stack } from "@mui/material";
|
||||
@@ -87,38 +87,36 @@ export default function AddParticipant({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EnteDrawer anchor="right" open={open} onClose={handleDrawerClose}>
|
||||
<Stack spacing={"4px"} py={"12px"}>
|
||||
<Titlebar
|
||||
onClose={onClose}
|
||||
title={
|
||||
type === COLLECTION_ROLE.VIEWER
|
||||
? t("ADD_VIEWERS")
|
||||
: t("ADD_COLLABORATORS")
|
||||
}
|
||||
onRootClose={handleRootClose}
|
||||
caption={collection.name}
|
||||
/>
|
||||
<AddParticipantForm
|
||||
onClose={onClose}
|
||||
callback={collectionShare}
|
||||
optionsList={nonSharedEmails}
|
||||
placeholder={t("ENTER_EMAIL")}
|
||||
fieldType="email"
|
||||
buttonText={
|
||||
type === COLLECTION_ROLE.VIEWER
|
||||
? t("ADD_VIEWERS")
|
||||
: t("ADD_COLLABORATORS")
|
||||
}
|
||||
submitButtonProps={{
|
||||
size: "large",
|
||||
sx: { mt: 1, mb: 2 },
|
||||
}}
|
||||
disableAutoFocus
|
||||
/>
|
||||
</Stack>
|
||||
</EnteDrawer>
|
||||
</>
|
||||
<SidebarDrawer anchor="right" open={open} onClose={handleDrawerClose}>
|
||||
<Stack spacing={"4px"} py={"12px"}>
|
||||
<Titlebar
|
||||
onClose={onClose}
|
||||
title={
|
||||
type === COLLECTION_ROLE.VIEWER
|
||||
? t("ADD_VIEWERS")
|
||||
: t("ADD_COLLABORATORS")
|
||||
}
|
||||
onRootClose={handleRootClose}
|
||||
caption={collection.name}
|
||||
/>
|
||||
<AddParticipantForm
|
||||
onClose={onClose}
|
||||
callback={collectionShare}
|
||||
optionsList={nonSharedEmails}
|
||||
placeholder={t("ENTER_EMAIL")}
|
||||
fieldType="email"
|
||||
buttonText={
|
||||
type === COLLECTION_ROLE.VIEWER
|
||||
? t("ADD_VIEWERS")
|
||||
: t("ADD_COLLABORATORS")
|
||||
}
|
||||
submitButtonProps={{
|
||||
size: "large",
|
||||
sx: { mt: 1, mb: 2 },
|
||||
}}
|
||||
disableAutoFocus
|
||||
/>
|
||||
</Stack>
|
||||
</SidebarDrawer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import {
|
||||
MenuItemDivider,
|
||||
MenuItemGroup,
|
||||
MenuSectionTitle,
|
||||
} from "@/base/components/Menu";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import {
|
||||
COLLECTION_ROLE,
|
||||
@@ -41,7 +41,7 @@ export default function ManageEmailShare({
|
||||
onRootClose,
|
||||
peopleCount,
|
||||
}: Iprops) {
|
||||
const appContext = useContext(AppContext);
|
||||
const { showLoadingBar, hideLoadingBar } = useContext(AppContext);
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
|
||||
const [addParticipantView, setAddParticipantView] = useState(false);
|
||||
@@ -80,11 +80,11 @@ export default function ManageEmailShare({
|
||||
|
||||
const collectionUnshare = async (email: string) => {
|
||||
try {
|
||||
appContext.startLoading();
|
||||
showLoadingBar();
|
||||
await unshareCollection(collection, email);
|
||||
await galleryContext.syncWithRemote(false, true);
|
||||
} finally {
|
||||
appContext.finishLoading();
|
||||
hideLoadingBar();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -116,7 +116,11 @@ export default function ManageEmailShare({
|
||||
|
||||
return (
|
||||
<>
|
||||
<EnteDrawer anchor="right" open={open} onClose={handleDrawerClose}>
|
||||
<SidebarDrawer
|
||||
anchor="right"
|
||||
open={open}
|
||||
onClose={handleDrawerClose}
|
||||
>
|
||||
<Stack spacing={"4px"} py={"12px"}>
|
||||
<Titlebar
|
||||
onClose={onClose}
|
||||
@@ -210,7 +214,7 @@ export default function ManageEmailShare({
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</EnteDrawer>
|
||||
</SidebarDrawer>
|
||||
<ManageParticipant
|
||||
collectionUnshare={collectionUnshare}
|
||||
open={manageParticipantView}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { MenuItemDivider, MenuItemGroup } from "@/base/components/Menu";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import log from "@/base/log";
|
||||
import type { Collection, CollectionUser } from "@/media/collection";
|
||||
@@ -129,82 +129,81 @@ export default function ManageParticipant({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EnteDrawer anchor="right" open={open} onClose={handleDrawerClose}>
|
||||
<Stack spacing={"4px"} py={"12px"}>
|
||||
<Titlebar
|
||||
onClose={onClose}
|
||||
title={t("MANAGE")}
|
||||
onRootClose={onRootClose}
|
||||
caption={selectedParticipant.email}
|
||||
/>
|
||||
<SidebarDrawer anchor="right" open={open} onClose={handleDrawerClose}>
|
||||
<Stack spacing={"4px"} py={"12px"}>
|
||||
<Titlebar
|
||||
onClose={onClose}
|
||||
title={t("MANAGE")}
|
||||
onRootClose={onRootClose}
|
||||
caption={selectedParticipant.email}
|
||||
/>
|
||||
|
||||
<Stack py={"20px"} px={"8px"} spacing={"32px"}>
|
||||
<Stack>
|
||||
<Stack py={"20px"} px={"8px"} spacing={"32px"}>
|
||||
<Stack>
|
||||
<Typography
|
||||
color="text.muted"
|
||||
variant="small"
|
||||
padding={1}
|
||||
>
|
||||
{t("ADDED_AS")}
|
||||
</Typography>
|
||||
|
||||
<MenuItemGroup>
|
||||
<EnteMenuItem
|
||||
fontWeight="normal"
|
||||
onClick={handleRoleChange("COLLABORATOR")}
|
||||
label={"Collaborator"}
|
||||
startIcon={<ModeEditIcon />}
|
||||
endIcon={
|
||||
selectedParticipant.role ===
|
||||
"COLLABORATOR" && <DoneIcon />
|
||||
}
|
||||
/>
|
||||
<MenuItemDivider hasIcon />
|
||||
|
||||
<EnteMenuItem
|
||||
fontWeight="normal"
|
||||
onClick={handleRoleChange("VIEWER")}
|
||||
label={"Viewer"}
|
||||
startIcon={<PhotoIcon />}
|
||||
endIcon={
|
||||
selectedParticipant.role === "VIEWER" && (
|
||||
<DoneIcon />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
|
||||
<Typography
|
||||
color="text.muted"
|
||||
variant="small"
|
||||
padding={1}
|
||||
>
|
||||
{t("COLLABORATOR_RIGHTS")}
|
||||
</Typography>
|
||||
|
||||
<Stack py={"30px"}>
|
||||
<Typography
|
||||
color="text.muted"
|
||||
variant="small"
|
||||
padding={1}
|
||||
>
|
||||
{t("ADDED_AS")}
|
||||
{t("REMOVE_PARTICIPANT_HEAD")}
|
||||
</Typography>
|
||||
|
||||
<MenuItemGroup>
|
||||
<EnteMenuItem
|
||||
color="critical"
|
||||
fontWeight="normal"
|
||||
onClick={handleRoleChange("COLLABORATOR")}
|
||||
label={"Collaborator"}
|
||||
startIcon={<ModeEditIcon />}
|
||||
endIcon={
|
||||
selectedParticipant.role ===
|
||||
"COLLABORATOR" && <DoneIcon />
|
||||
}
|
||||
/>
|
||||
<MenuItemDivider hasIcon />
|
||||
|
||||
<EnteMenuItem
|
||||
fontWeight="normal"
|
||||
onClick={handleRoleChange("VIEWER")}
|
||||
label={"Viewer"}
|
||||
startIcon={<PhotoIcon />}
|
||||
endIcon={
|
||||
selectedParticipant.role ===
|
||||
"VIEWER" && <DoneIcon />
|
||||
}
|
||||
onClick={removeParticipant}
|
||||
label={"Remove"}
|
||||
startIcon={<BlockIcon />}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
|
||||
<Typography
|
||||
color="text.muted"
|
||||
variant="small"
|
||||
padding={1}
|
||||
>
|
||||
{t("COLLABORATOR_RIGHTS")}
|
||||
</Typography>
|
||||
|
||||
<Stack py={"30px"}>
|
||||
<Typography
|
||||
color="text.muted"
|
||||
variant="small"
|
||||
padding={1}
|
||||
>
|
||||
{t("REMOVE_PARTICIPANT_HEAD")}
|
||||
</Typography>
|
||||
|
||||
<MenuItemGroup>
|
||||
<EnteMenuItem
|
||||
color="critical"
|
||||
fontWeight="normal"
|
||||
onClick={removeParticipant}
|
||||
label={"Remove"}
|
||||
startIcon={<BlockIcon />}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</EnteDrawer>
|
||||
</>
|
||||
</Stack>
|
||||
</SidebarDrawer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import type { CollectionSummary } from "@/new/photos/services/collection/ui";
|
||||
@@ -32,7 +32,7 @@ function CollectionShare({ collectionSummary, ...props }: Props) {
|
||||
const { type } = collectionSummary;
|
||||
|
||||
return (
|
||||
<EnteDrawer
|
||||
<SidebarDrawer
|
||||
anchor="right"
|
||||
open={props.open}
|
||||
onClose={handleDrawerClose}
|
||||
@@ -75,7 +75,7 @@ function CollectionShare({ collectionSummary, ...props }: Props) {
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</EnteDrawer>
|
||||
</SidebarDrawer>
|
||||
);
|
||||
}
|
||||
export default CollectionShare;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { MenuItemDivider, MenuItemGroup } from "@/base/components/Menu";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import type {
|
||||
Collection,
|
||||
@@ -68,7 +68,7 @@ export function ManageDeviceLimit({
|
||||
endIcon={<ChevronRight />}
|
||||
/>
|
||||
|
||||
<EnteDrawer
|
||||
<SidebarDrawer
|
||||
anchor="right"
|
||||
open={isChangeDeviceLimitVisible}
|
||||
onClose={handleDrawerClose}
|
||||
@@ -100,7 +100,7 @@ export function ManageDeviceLimit({
|
||||
</MenuItemGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</EnteDrawer>
|
||||
</SidebarDrawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { MenuItemDivider, MenuItemGroup } from "@/base/components/Menu";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import type {
|
||||
Collection,
|
||||
@@ -86,24 +86,32 @@ export default function ManagePublicShareOptions({
|
||||
navigator.clipboard.writeText(text);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<EnteDrawer anchor="right" open={open} onClose={handleDrawerClose}>
|
||||
<Stack spacing={"4px"} py={"12px"}>
|
||||
<Titlebar
|
||||
onClose={onClose}
|
||||
title={t("share_album")}
|
||||
onRootClose={onRootClose}
|
||||
/>
|
||||
<Stack py={"20px"} px={"8px"} spacing={"32px"}>
|
||||
<Stack spacing={3}>
|
||||
<ManagePublicCollect
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
updatePublicShareURLHelper={
|
||||
updatePublicShareURLHelper
|
||||
}
|
||||
/>
|
||||
<ManageLinkExpiry
|
||||
<SidebarDrawer anchor="right" open={open} onClose={handleDrawerClose}>
|
||||
<Stack spacing={"4px"} py={"12px"}>
|
||||
<Titlebar
|
||||
onClose={onClose}
|
||||
title={t("share_album")}
|
||||
onRootClose={onRootClose}
|
||||
/>
|
||||
<Stack py={"20px"} px={"8px"} spacing={"32px"}>
|
||||
<Stack spacing={3}>
|
||||
<ManagePublicCollect
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
updatePublicShareURLHelper={
|
||||
updatePublicShareURLHelper
|
||||
}
|
||||
/>
|
||||
<ManageLinkExpiry
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
updatePublicShareURLHelper={
|
||||
updatePublicShareURLHelper
|
||||
}
|
||||
onRootClose={onRootClose}
|
||||
/>
|
||||
<MenuItemGroup>
|
||||
<ManageDeviceLimit
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
updatePublicShareURLHelper={
|
||||
@@ -111,65 +119,53 @@ export default function ManagePublicShareOptions({
|
||||
}
|
||||
onRootClose={onRootClose}
|
||||
/>
|
||||
<MenuItemGroup>
|
||||
<ManageDeviceLimit
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
updatePublicShareURLHelper={
|
||||
updatePublicShareURLHelper
|
||||
}
|
||||
onRootClose={onRootClose}
|
||||
/>
|
||||
<MenuItemDivider />
|
||||
<ManageDownloadAccess
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
updatePublicShareURLHelper={
|
||||
updatePublicShareURLHelper
|
||||
}
|
||||
/>
|
||||
<MenuItemDivider />
|
||||
<ManageLinkPassword
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
updatePublicShareURLHelper={
|
||||
updatePublicShareURLHelper
|
||||
}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
<MenuItemGroup>
|
||||
<EnteMenuItem
|
||||
startIcon={<ContentCopyIcon />}
|
||||
onClick={copyToClipboardHelper(
|
||||
publicShareUrl,
|
||||
)}
|
||||
label={t("COPY_LINK")}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
<MenuItemGroup>
|
||||
<EnteMenuItem
|
||||
color="critical"
|
||||
startIcon={<RemoveCircleOutline />}
|
||||
onClick={disablePublicSharing}
|
||||
label={t("REMOVE_LINK")}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
</Stack>
|
||||
{sharableLinkError && (
|
||||
<Typography
|
||||
textAlign={"center"}
|
||||
variant="small"
|
||||
sx={{
|
||||
color: (theme) => theme.colors.danger.A700,
|
||||
mt: 0.5,
|
||||
}}
|
||||
>
|
||||
{sharableLinkError}
|
||||
</Typography>
|
||||
)}
|
||||
<MenuItemDivider />
|
||||
<ManageDownloadAccess
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
updatePublicShareURLHelper={
|
||||
updatePublicShareURLHelper
|
||||
}
|
||||
/>
|
||||
<MenuItemDivider />
|
||||
<ManageLinkPassword
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
updatePublicShareURLHelper={
|
||||
updatePublicShareURLHelper
|
||||
}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
<MenuItemGroup>
|
||||
<EnteMenuItem
|
||||
startIcon={<ContentCopyIcon />}
|
||||
onClick={copyToClipboardHelper(publicShareUrl)}
|
||||
label={t("COPY_LINK")}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
<MenuItemGroup>
|
||||
<EnteMenuItem
|
||||
color="critical"
|
||||
startIcon={<RemoveCircleOutline />}
|
||||
onClick={disablePublicSharing}
|
||||
label={t("REMOVE_LINK")}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
</Stack>
|
||||
{sharableLinkError && (
|
||||
<Typography
|
||||
textAlign={"center"}
|
||||
variant="small"
|
||||
sx={{
|
||||
color: (theme) => theme.colors.danger.A700,
|
||||
mt: 0.5,
|
||||
}}
|
||||
>
|
||||
{sharableLinkError}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</EnteDrawer>
|
||||
</>
|
||||
</Stack>
|
||||
</SidebarDrawer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { MenuItemDivider, MenuItemGroup } from "@/base/components/Menu";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import type {
|
||||
Collection,
|
||||
@@ -84,7 +84,7 @@ export function ManageLinkExpiry({
|
||||
}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
<EnteDrawer
|
||||
<SidebarDrawer
|
||||
anchor="right"
|
||||
open={shareExpiryOptionsModalView}
|
||||
onClose={handleDrawerClose}
|
||||
@@ -115,7 +115,7 @@ export function ManageLinkExpiry({
|
||||
</MenuItemGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</EnteDrawer>
|
||||
</SidebarDrawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import type { MiniDialogAttributes } from "@/base/components/MiniDialog";
|
||||
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import { EllipsizedTypography } from "@/base/components/Typography";
|
||||
import { useModalVisibility } from "@/base/components/utils/modal";
|
||||
@@ -400,7 +400,7 @@ const confirmDisableMapsDialogAttributes = (
|
||||
});
|
||||
|
||||
const FileInfoSidebar = styled((props: DialogProps) => (
|
||||
<EnteDrawer {...props} anchor="right" />
|
||||
<SidebarDrawer {...props} anchor="right" />
|
||||
))({
|
||||
zIndex: fileInfoDrawerZIndex,
|
||||
"& .MuiPaper-root": {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import {
|
||||
MenuItemDivider,
|
||||
MenuItemGroup,
|
||||
MenuSectionTitle,
|
||||
} from "@/base/components/Menu";
|
||||
import type { MiniDialogAttributes } from "@/base/components/MiniDialog";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { nameAndExtension } from "@/base/file";
|
||||
import log from "@/base/log";
|
||||
import { downloadAndRevokeObjectURL } from "@/base/utils/web";
|
||||
@@ -614,7 +614,7 @@ const ImageEditorOverlay = (props: IProps) => {
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<EnteDrawer
|
||||
<SidebarDrawer
|
||||
variant="persistent"
|
||||
anchor="right"
|
||||
open={showControlsDrawer}
|
||||
@@ -722,7 +722,7 @@ const ImageEditorOverlay = (props: IProps) => {
|
||||
title={t("PHOTO_EDIT_REQUIRED_TO_SAVE")}
|
||||
/>
|
||||
)}
|
||||
</EnteDrawer>
|
||||
</SidebarDrawer>
|
||||
</Backdrop>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -118,7 +118,8 @@ export interface PhotoViewerProps {
|
||||
|
||||
function PhotoViewer(props: PhotoViewerProps) {
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
const appContext = useContext(AppContext);
|
||||
const { showLoadingBar, hideLoadingBar, setDialogMessage } =
|
||||
useContext(AppContext);
|
||||
const publicCollectionGalleryContext = useContext(
|
||||
PublicCollectionGalleryContext,
|
||||
);
|
||||
@@ -537,9 +538,12 @@ function PhotoViewer(props: PhotoViewerProps) {
|
||||
|
||||
const trashFile = async (file: EnteFile) => {
|
||||
try {
|
||||
appContext.startLoading();
|
||||
await trashFiles([file]);
|
||||
appContext.finishLoading();
|
||||
showLoadingBar();
|
||||
try {
|
||||
await trashFiles([file]);
|
||||
} finally {
|
||||
hideLoadingBar();
|
||||
}
|
||||
markTempDeleted?.([file]);
|
||||
updateItems(props.items.filter((item) => item.id !== file.id));
|
||||
needUpdate.current = true;
|
||||
@@ -552,7 +556,7 @@ function PhotoViewer(props: PhotoViewerProps) {
|
||||
if (!file || !isOwnFile || props.isTrashCollection) {
|
||||
return;
|
||||
}
|
||||
appContext.setDialogMessage(getTrashFileMessage(() => trashFile(file)));
|
||||
setDialogMessage(getTrashFileMessage(() => trashFile(file)));
|
||||
};
|
||||
|
||||
const handleArrowClick = (
|
||||
@@ -683,9 +687,9 @@ function PhotoViewer(props: PhotoViewerProps) {
|
||||
|
||||
const copyToClipboardHelper = async (file: EnteFile) => {
|
||||
if (file && props.enableDownload && shouldShowCopyOption) {
|
||||
appContext.startLoading();
|
||||
showLoadingBar();
|
||||
await copyFileToClipboard(file.src);
|
||||
appContext.finishLoading();
|
||||
hideLoadingBar();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { MenuItemGroup, MenuSectionTitle } from "@/base/components/Menu";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import type { NestedDrawerVisibilityProps } from "@/base/components/utils/modal";
|
||||
import { AppContext } from "@/new/photos/types/context";
|
||||
@@ -33,7 +33,7 @@ export const AdvancedSettings: React.FC<NestedDrawerVisibilityProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<EnteDrawer
|
||||
<SidebarDrawer
|
||||
transitionDuration={0}
|
||||
open={open}
|
||||
onClose={handleDrawerClose}
|
||||
@@ -66,6 +66,6 @@ export const AdvancedSettings: React.FC<NestedDrawerVisibilityProps> = ({
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</EnteDrawer>
|
||||
</SidebarDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { MenuItemGroup } from "@/base/components/Menu";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import type { NestedDrawerVisibilityProps } from "@/base/components/utils/modal";
|
||||
import log from "@/base/log";
|
||||
@@ -54,7 +54,7 @@ export const MapSettings: React.FC<NestedDrawerVisibilityProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<EnteDrawer
|
||||
<SidebarDrawer
|
||||
transitionDuration={0}
|
||||
open={open}
|
||||
onClose={handleDrawerClose}
|
||||
@@ -90,7 +90,7 @@ export const MapSettings: React.FC<NestedDrawerVisibilityProps> = ({
|
||||
onClose={closeModifyMapEnabled}
|
||||
onRootClose={handleRootClose}
|
||||
/>
|
||||
</EnteDrawer>
|
||||
</SidebarDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -132,7 +132,7 @@ const ModifyMapEnabled = ({ open, onClose, onRootClose, mapEnabled }) => {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<EnteDrawer
|
||||
<SidebarDrawer
|
||||
anchor="left"
|
||||
transitionDuration={0}
|
||||
open={open}
|
||||
@@ -156,7 +156,7 @@ const ModifyMapEnabled = ({ open, onClose, onRootClose, mapEnabled }) => {
|
||||
onRootClose={handleRootClose}
|
||||
/>
|
||||
)}
|
||||
</EnteDrawer>
|
||||
</SidebarDrawer>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { MenuItemGroup, MenuSectionTitle } from "@/base/components/Menu";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import {
|
||||
useModalVisibility,
|
||||
@@ -13,13 +13,14 @@ import {
|
||||
} from "@/base/i18n";
|
||||
import { MLSettings } from "@/new/photos/components/MLSettings";
|
||||
import { isMLSupported } from "@/new/photos/services/ml";
|
||||
import { syncSettings } from "@/new/photos/services/settings";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import ChevronRight from "@mui/icons-material/ChevronRight";
|
||||
import ScienceIcon from "@mui/icons-material/Science";
|
||||
import { Box, DialogProps, Stack } from "@mui/material";
|
||||
import DropdownInput from "components/DropdownInput";
|
||||
import { t } from "i18next";
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { AdvancedSettings } from "./AdvancedSettings";
|
||||
import { MapSettings } from "./MapSetting";
|
||||
|
||||
@@ -37,6 +38,10 @@ export const Preferences: React.FC<NestedDrawerVisibilityProps> = ({
|
||||
const { show: showMLSettings, props: mlSettingsVisibilityProps } =
|
||||
useModalVisibility();
|
||||
|
||||
useEffect(() => {
|
||||
if (open) void syncSettings();
|
||||
}, [open]);
|
||||
|
||||
const handleRootClose = () => {
|
||||
onClose();
|
||||
onRootClose();
|
||||
@@ -51,7 +56,7 @@ export const Preferences: React.FC<NestedDrawerVisibilityProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<EnteDrawer
|
||||
<SidebarDrawer
|
||||
transitionDuration={0}
|
||||
open={open}
|
||||
onClose={handleDrawerClose}
|
||||
@@ -108,7 +113,7 @@ export const Preferences: React.FC<NestedDrawerVisibilityProps> = ({
|
||||
{...mlSettingsVisibilityProps}
|
||||
onRootClose={handleRootClose}
|
||||
/>
|
||||
</EnteDrawer>
|
||||
</SidebarDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -166,6 +171,6 @@ const localeName = (locale: SupportedLocale) => {
|
||||
case "lt-LT":
|
||||
return "Lietuvių kalba";
|
||||
case "uk-UA":
|
||||
return "українська";
|
||||
return "Українська";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { RecoveryKey } from "@/accounts/components/RecoveryKey";
|
||||
import { openAccountsManagePasskeysPage } from "@/accounts/services/passkey";
|
||||
import { isDesktop } from "@/base/app";
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { EnteLogo } from "@/base/components/EnteLogo";
|
||||
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { useModalVisibility } from "@/base/components/utils/modal";
|
||||
import log from "@/base/log";
|
||||
import { savedLogs } from "@/base/log-web";
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
TRASH_SECTION,
|
||||
} from "@/new/photos/services/collection";
|
||||
import type { CollectionSummaries } from "@/new/photos/services/collection/ui";
|
||||
import { isInternalUser } from "@/new/photos/services/settings";
|
||||
import { AppContext, useAppContext } from "@/new/photos/types/context";
|
||||
import { initiateEmail, openURL } from "@/new/photos/utils/web";
|
||||
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
|
||||
@@ -93,7 +94,7 @@ export default function Sidebar({
|
||||
closeSidebar,
|
||||
}: Iprops) {
|
||||
return (
|
||||
<DrawerSidebar open={sidebarView} onClose={closeSidebar}>
|
||||
<RootSidebarDrawer open={sidebarView} onClose={closeSidebar}>
|
||||
<HeaderSection closeSidebar={closeSidebar} />
|
||||
<Divider />
|
||||
<UserDetailsSection sidebarView={sidebarView} />
|
||||
@@ -110,18 +111,16 @@ export default function Sidebar({
|
||||
<Divider />
|
||||
<DebugSection />
|
||||
</Stack>
|
||||
</DrawerSidebar>
|
||||
</RootSidebarDrawer>
|
||||
);
|
||||
}
|
||||
|
||||
const DrawerSidebar = styled(EnteDrawer)(({ theme }) => ({
|
||||
const RootSidebarDrawer = styled(SidebarDrawer)(({ theme }) => ({
|
||||
"& .MuiPaper-root": {
|
||||
padding: theme.spacing(1.5),
|
||||
},
|
||||
}));
|
||||
|
||||
DrawerSidebar.defaultProps = { anchor: "left" };
|
||||
|
||||
interface HeaderSectionProps {
|
||||
closeSidebar: () => void;
|
||||
}
|
||||
@@ -426,15 +425,13 @@ interface UtilitySectionProps {
|
||||
|
||||
const UtilitySection: React.FC<UtilitySectionProps> = ({ closeSidebar }) => {
|
||||
const router = useRouter();
|
||||
const appContext = useContext(AppContext);
|
||||
const {
|
||||
startLoading,
|
||||
watchFolderView,
|
||||
setWatchFolderView,
|
||||
themeColor,
|
||||
setThemeColor,
|
||||
showMiniDialog,
|
||||
} = appContext;
|
||||
} = useAppContext();
|
||||
|
||||
const { show: showRecoveryKey, props: recoveryKeyVisibilityProps } =
|
||||
useModalVisibility();
|
||||
@@ -484,7 +481,7 @@ const UtilitySection: React.FC<UtilitySectionProps> = ({ closeSidebar }) => {
|
||||
onClick={showRecoveryKey}
|
||||
label={t("recovery_key")}
|
||||
/>
|
||||
{isInternalUserViaEmailCheck() && (
|
||||
{isInternalUser() && (
|
||||
<EnteMenuItem
|
||||
onClick={toggleTheme}
|
||||
variant="secondary"
|
||||
@@ -536,7 +533,6 @@ const UtilitySection: React.FC<UtilitySectionProps> = ({ closeSidebar }) => {
|
||||
<TwoFactorModal
|
||||
{...twoFactorVisibilityProps}
|
||||
closeSidebar={closeSidebar}
|
||||
setLoading={startLoading}
|
||||
/>
|
||||
{isElectron() && (
|
||||
<WatchFolder
|
||||
@@ -656,7 +652,7 @@ const DebugSection: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{isInternalUserViaEmailCheck() && (
|
||||
{isInternalUser() && (
|
||||
<EnteMenuItem
|
||||
variant="secondary"
|
||||
onClick={testUpload}
|
||||
@@ -677,11 +673,3 @@ const DebugSection: React.FC = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: Legacy synchronous check, use the one for feature-flags.ts instead.
|
||||
const isInternalUserViaEmailCheck = () => {
|
||||
const userEmail = getData(LS_KEYS.USER)?.email;
|
||||
if (!userEmail) return false;
|
||||
|
||||
return userEmail.endsWith("@ente.io");
|
||||
};
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { t } from "i18next";
|
||||
import { useContext } from "react";
|
||||
|
||||
import { disableTwoFactor } from "@/accounts/api/user";
|
||||
import { AppContext } from "@/new/photos/types/context";
|
||||
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
|
||||
import { LS_KEYS, getData, setLSUser } from "@ente/shared/storage/localStorage";
|
||||
import { Button, Grid } from "@mui/material";
|
||||
import router from "next/router";
|
||||
|
||||
interface Iprops {
|
||||
closeDialog: () => void;
|
||||
}
|
||||
|
||||
export default function TwoFactorModalManageSection(props: Iprops) {
|
||||
const { closeDialog } = props;
|
||||
const { setDialogMessage } = useContext(AppContext);
|
||||
|
||||
const warnTwoFactorDisable = async () => {
|
||||
setDialogMessage({
|
||||
title: t("DISABLE_TWO_FACTOR"),
|
||||
|
||||
content: t("DISABLE_TWO_FACTOR_MESSAGE"),
|
||||
close: { text: t("cancel") },
|
||||
proceed: {
|
||||
variant: "critical",
|
||||
text: t("disable"),
|
||||
action: twoFactorDisable,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const twoFactorDisable = async () => {
|
||||
try {
|
||||
await disableTwoFactor();
|
||||
await setLSUser({
|
||||
...getData(LS_KEYS.USER),
|
||||
isTwoFactorEnabled: false,
|
||||
});
|
||||
closeDialog();
|
||||
} catch (e) {
|
||||
setDialogMessage({
|
||||
title: t("TWO_FACTOR_DISABLE_FAILED"),
|
||||
close: {},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const warnTwoFactorReconfigure = async () => {
|
||||
setDialogMessage({
|
||||
title: t("UPDATE_TWO_FACTOR"),
|
||||
|
||||
content: t("UPDATE_TWO_FACTOR_MESSAGE"),
|
||||
close: { text: t("cancel") },
|
||||
proceed: {
|
||||
variant: "accent",
|
||||
text: t("UPDATE"),
|
||||
action: reconfigureTwoFactor,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const reconfigureTwoFactor = async () => {
|
||||
closeDialog();
|
||||
router.push(PAGES.TWO_FACTOR_SETUP);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
mb={1.5}
|
||||
rowSpacing={1}
|
||||
container
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Grid item sm={9} xs={12}>
|
||||
{t("UPDATE_TWO_FACTOR_LABEL")}
|
||||
</Grid>
|
||||
<Grid item sm={3} xs={12}>
|
||||
<Button
|
||||
color={"accent"}
|
||||
onClick={warnTwoFactorReconfigure}
|
||||
size="large"
|
||||
>
|
||||
{t("reconfigure")}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid
|
||||
rowSpacing={1}
|
||||
container
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Grid item sm={9} xs={12}>
|
||||
{t("DISABLE_TWO_FACTOR_LABEL")}{" "}
|
||||
</Grid>
|
||||
|
||||
<Grid item sm={3} xs={12}>
|
||||
<Button
|
||||
color="critical"
|
||||
onClick={warnTwoFactorDisable}
|
||||
size="large"
|
||||
>
|
||||
{t("disable")}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
|
||||
import LockIcon from "@mui/icons-material/Lock";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import { Button, Typography } from "@mui/material";
|
||||
|
||||
interface Iprops {
|
||||
closeDialog: () => void;
|
||||
}
|
||||
|
||||
export default function TwoFactorModalSetupSection({ closeDialog }: Iprops) {
|
||||
const router = useRouter();
|
||||
const redirectToTwoFactorSetup = () => {
|
||||
closeDialog();
|
||||
router.push(PAGES.TWO_FACTOR_SETUP);
|
||||
};
|
||||
|
||||
return (
|
||||
<VerticallyCentered sx={{ mb: 2 }}>
|
||||
<LockIcon sx={{ fontSize: (theme) => theme.spacing(5), mb: 2 }} />
|
||||
<Typography mb={4}>{t("TWO_FACTOR_INFO")}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
size="large"
|
||||
onClick={redirectToTwoFactorSetup}
|
||||
>
|
||||
{t("ENABLE_TWO_FACTOR")}
|
||||
</Button>
|
||||
</VerticallyCentered>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,23 @@
|
||||
import { disableTwoFactor } from "@/accounts/api/user";
|
||||
import type { ModalVisibilityProps } from "@/base/components/utils/modal";
|
||||
import { AppContext } from "@/new/photos/types/context";
|
||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import DialogTitleWithCloseButton from "@ente/shared/components/DialogBox/TitleWithCloseButton";
|
||||
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
|
||||
import { LS_KEYS, getData, setLSUser } from "@ente/shared/storage/localStorage";
|
||||
import { Dialog, DialogContent, styled } from "@mui/material";
|
||||
import LockIcon from "@mui/icons-material/Lock";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
Grid,
|
||||
Typography,
|
||||
styled,
|
||||
} from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
import router, { useRouter } from "next/router";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { getTwoFactorStatus } from "services/userService";
|
||||
import { SetLoading } from "types/gallery";
|
||||
import TwoFactorModalManageSection from "./Manage";
|
||||
import TwoFactorModalSetupSection from "./Setup";
|
||||
|
||||
const TwoFactorDialog = styled(Dialog)(({ theme }) => ({
|
||||
"& .MuiDialogContent-root": {
|
||||
@@ -16,7 +26,6 @@ const TwoFactorDialog = styled(Dialog)(({ theme }) => ({
|
||||
}));
|
||||
|
||||
type Props = ModalVisibilityProps & {
|
||||
setLoading: SetLoading;
|
||||
closeSidebar: () => void;
|
||||
};
|
||||
|
||||
@@ -68,4 +77,137 @@ function TwoFactorModal(props: Props) {
|
||||
</TwoFactorDialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default TwoFactorModal;
|
||||
|
||||
interface TwoFactorModalSetupSectionProps {
|
||||
closeDialog: () => void;
|
||||
}
|
||||
|
||||
function TwoFactorModalSetupSection({
|
||||
closeDialog,
|
||||
}: TwoFactorModalSetupSectionProps) {
|
||||
const router = useRouter();
|
||||
const redirectToTwoFactorSetup = () => {
|
||||
closeDialog();
|
||||
router.push(PAGES.TWO_FACTOR_SETUP);
|
||||
};
|
||||
|
||||
return (
|
||||
<VerticallyCentered sx={{ mb: 2 }}>
|
||||
<LockIcon sx={{ fontSize: (theme) => theme.spacing(5), mb: 2 }} />
|
||||
<Typography mb={4}>{t("TWO_FACTOR_INFO")}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
size="large"
|
||||
onClick={redirectToTwoFactorSetup}
|
||||
>
|
||||
{t("ENABLE_TWO_FACTOR")}
|
||||
</Button>
|
||||
</VerticallyCentered>
|
||||
);
|
||||
}
|
||||
|
||||
interface TwoFactorModalManageSectionProps {
|
||||
closeDialog: () => void;
|
||||
}
|
||||
|
||||
function TwoFactorModalManageSection(props: TwoFactorModalManageSectionProps) {
|
||||
const { closeDialog } = props;
|
||||
const { setDialogMessage } = useContext(AppContext);
|
||||
|
||||
const warnTwoFactorDisable = async () => {
|
||||
setDialogMessage({
|
||||
title: t("DISABLE_TWO_FACTOR"),
|
||||
|
||||
content: t("DISABLE_TWO_FACTOR_MESSAGE"),
|
||||
close: { text: t("cancel") },
|
||||
proceed: {
|
||||
variant: "critical",
|
||||
text: t("disable"),
|
||||
action: twoFactorDisable,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const twoFactorDisable = async () => {
|
||||
try {
|
||||
await disableTwoFactor();
|
||||
await setLSUser({
|
||||
...getData(LS_KEYS.USER),
|
||||
isTwoFactorEnabled: false,
|
||||
});
|
||||
closeDialog();
|
||||
} catch (e) {
|
||||
setDialogMessage({
|
||||
title: t("TWO_FACTOR_DISABLE_FAILED"),
|
||||
close: {},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const warnTwoFactorReconfigure = async () => {
|
||||
setDialogMessage({
|
||||
title: t("UPDATE_TWO_FACTOR"),
|
||||
|
||||
content: t("UPDATE_TWO_FACTOR_MESSAGE"),
|
||||
close: { text: t("cancel") },
|
||||
proceed: {
|
||||
variant: "accent",
|
||||
text: t("UPDATE"),
|
||||
action: reconfigureTwoFactor,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const reconfigureTwoFactor = async () => {
|
||||
closeDialog();
|
||||
router.push(PAGES.TWO_FACTOR_SETUP);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
mb={1.5}
|
||||
rowSpacing={1}
|
||||
container
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Grid item sm={9} xs={12}>
|
||||
{t("UPDATE_TWO_FACTOR_LABEL")}
|
||||
</Grid>
|
||||
<Grid item sm={3} xs={12}>
|
||||
<Button
|
||||
color={"accent"}
|
||||
onClick={warnTwoFactorReconfigure}
|
||||
size="large"
|
||||
>
|
||||
{t("reconfigure")}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid
|
||||
rowSpacing={1}
|
||||
container
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Grid item sm={9} xs={12}>
|
||||
{t("DISABLE_TWO_FACTOR_LABEL")}{" "}
|
||||
</Grid>
|
||||
|
||||
<Grid item sm={3} xs={12}>
|
||||
<Button
|
||||
color="critical"
|
||||
onClick={warnTwoFactorDisable}
|
||||
size="large"
|
||||
>
|
||||
{t("disable")}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
export default function ObjectIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
>
|
||||
<path d="M11.499 12.03v11.971l-10.5-5.603v-11.835l10.5 5.467zm11.501 6.368l-10.501 5.602v-11.968l10.501-5.404v11.77zm-16.889-15.186l10.609 5.524-4.719 2.428-10.473-5.453 4.583-2.499zm16.362 2.563l-4.664 2.4-10.641-5.54 4.831-2.635 10.474 5.775z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
ObjectIcon.defaultProps = {
|
||||
height: 20,
|
||||
width: 20,
|
||||
viewBox: "0 0 24 24",
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
export default function TextIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
>
|
||||
<path d="M22 0h-20v6h1.999c0-1.174.397-3 2.001-3h4v16.874c0 1.174-.825 2.126-2 2.126h-1v2h9.999v-2h-.999c-1.174 0-2-.952-2-2.126v-16.874h4c1.649 0 2.02 1.826 2.02 3h1.98v-6z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
TextIcon.defaultProps = {
|
||||
height: 16,
|
||||
width: 16,
|
||||
viewBox: "0 0 28 28",
|
||||
};
|
||||
@@ -1,80 +0,0 @@
|
||||
import SvgIcon from "@mui/material/SvgIcon";
|
||||
|
||||
export const UnPinIcon = (props) => {
|
||||
return (
|
||||
<SvgIcon
|
||||
id="eNi9tomYy271"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
shape-rendering="geometricPrecision"
|
||||
text-rendering="geometricPrecision"
|
||||
{...props}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="eNi9tomYy273-fill"
|
||||
x1="0"
|
||||
y1="0.5"
|
||||
x2="1"
|
||||
y2="0.5"
|
||||
spreadMethod="pad"
|
||||
gradientUnits="objectBoundingBox"
|
||||
gradientTransform="translate(0 0)"
|
||||
>
|
||||
<stop
|
||||
id="eNi9tomYy273-fill-0"
|
||||
offset="100%"
|
||||
stop-color="#fff"
|
||||
/>
|
||||
<stop
|
||||
id="eNi9tomYy273-fill-1"
|
||||
offset="100%"
|
||||
stop-color="#fff"
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M634,-448l86,77v60h-210v241l-30,30-30-30v-241h-210v-60l80-77v-332h-50v-60h414v60h-50v332Zm-313,77h312l-59-55v-354h-194v354l-59,55Zm156,0" />
|
||||
<rect
|
||||
width="26.347306"
|
||||
height="26.347306"
|
||||
rx="0"
|
||||
ry="0"
|
||||
transform="matrix(29.14543 20.40785-1.147153 1.638304 147.37857-852.859017)"
|
||||
fill="url(#eNi9tomYy273-fill)"
|
||||
stroke-width="0"
|
||||
/>
|
||||
<rect
|
||||
width="65.868264"
|
||||
height="8.383234"
|
||||
rx="0"
|
||||
ry="0"
|
||||
transform="matrix(.961684 0.648664-.559193 0.829038 575.09991-562.860814)"
|
||||
fill="#f8eeee"
|
||||
stroke-width="0"
|
||||
/>
|
||||
<rect
|
||||
width="65.868264"
|
||||
height="8.383234"
|
||||
rx="0"
|
||||
ry="0"
|
||||
transform="matrix(.961684 0.648664-.559193 0.829038 322.549012-741.890754)"
|
||||
fill="#fff"
|
||||
stroke-width="0"
|
||||
/>
|
||||
<rect
|
||||
width="96"
|
||||
height="96"
|
||||
rx="0"
|
||||
ry="0"
|
||||
transform="translate(1379.418154-389.125449)"
|
||||
fill="#d2dbed"
|
||||
stroke-width="0"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
};
|
||||
|
||||
UnPinIcon.defaultProps = {
|
||||
height: 20,
|
||||
width: 20,
|
||||
};
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
updateAvailableForDownloadDialogAttributes,
|
||||
updateReadyToInstallDialogAttributes,
|
||||
} from "@/new/photos/components/utils/download";
|
||||
import { useLoadingBar } from "@/new/photos/components/utils/use-loading-bar";
|
||||
import { photosDialogZIndex } from "@/new/photos/components/utils/z-index";
|
||||
import DownloadManager from "@/new/photos/services/download";
|
||||
import { runMigrations } from "@/new/photos/services/migrations";
|
||||
@@ -51,7 +52,7 @@ import isElectron from "is-electron";
|
||||
import type { AppProps } from "next/app";
|
||||
import { useRouter } from "next/router";
|
||||
import "photoswipe/dist/photoswipe.css";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import LoadingBar from "react-top-loading-bar";
|
||||
import { resumeExportsIfNeeded } from "services/export";
|
||||
import { photosLogout } from "services/logout";
|
||||
@@ -71,8 +72,6 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
);
|
||||
const [showNavbar, setShowNavBar] = useState(false);
|
||||
const [mapEnabled, setMapEnabled] = useState(false);
|
||||
const isLoadingBarRunning = useRef(false);
|
||||
const loadingBar = useRef(null);
|
||||
const [dialogMessage, setDialogMessage] = useState<DialogBoxAttributes>();
|
||||
const [messageDialogView, setMessageDialogView] = useState(false);
|
||||
const [watchFolderView, setWatchFolderView] = useState(false);
|
||||
@@ -83,6 +82,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
useState<NotificationAttributes>(null);
|
||||
|
||||
const { showMiniDialog, miniDialogProps } = useAttributedMiniDialog();
|
||||
const { loadingBarRef, showLoadingBar, hideLoadingBar } = useLoadingBar();
|
||||
const [themeColor, setThemeColor] = useLocalState(
|
||||
LS_KEYS.THEME,
|
||||
THEME_COLOR.DARK,
|
||||
@@ -213,17 +213,6 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
setMapEnabled(enabled);
|
||||
};
|
||||
|
||||
const startLoading = () => {
|
||||
!isLoadingBarRunning.current && loadingBar.current?.continuousStart();
|
||||
isLoadingBarRunning.current = true;
|
||||
};
|
||||
const finishLoading = () => {
|
||||
setTimeout(() => {
|
||||
isLoadingBarRunning.current && loadingBar.current?.complete();
|
||||
isLoadingBarRunning.current = false;
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Use `onGenericError` instead.
|
||||
const somethingWentWrong = useCallback(
|
||||
() =>
|
||||
@@ -246,8 +235,8 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
|
||||
const appContext = {
|
||||
showNavBar,
|
||||
startLoading, // <- changes on each render (TODO Fix)
|
||||
finishLoading, // <- changes on each render
|
||||
showLoadingBar,
|
||||
hideLoadingBar,
|
||||
setDialogMessage,
|
||||
watchFolderView,
|
||||
setWatchFolderView,
|
||||
@@ -278,7 +267,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
<MessageContainer>
|
||||
{isI18nReady && offline && t("OFFLINE_MSG")}
|
||||
</MessageContainer>
|
||||
<LoadingBar color="#51cd7c" ref={loadingBar} />
|
||||
<LoadingBar color="#51cd7c" ref={loadingBarRef} />
|
||||
|
||||
<DialogBox
|
||||
sx={{ zIndex: photosDialogZIndex }}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
|
||||
import { ALL_SECTION } from "@/new/photos/services/collection";
|
||||
import { createFileCollectionIDs } from "@/new/photos/services/file";
|
||||
import { getLocalFiles } from "@/new/photos/services/files";
|
||||
import { AppContext } from "@/new/photos/types/context";
|
||||
import { useAppContext } from "@/new/photos/types/context";
|
||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
|
||||
import { ApiError } from "@ente/shared/error";
|
||||
@@ -16,7 +16,7 @@ import DeduplicateOptions from "components/pages/dedupe/SelectedFileOptions";
|
||||
import PhotoFrame from "components/PhotoFrame";
|
||||
import { t } from "i18next";
|
||||
import { default as Router, default as router } from "next/router";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import { createContext, useEffect, useState } from "react";
|
||||
import {
|
||||
getAllLatestCollections,
|
||||
getLocalCollections,
|
||||
@@ -40,8 +40,8 @@ export const Info = styled("div")`
|
||||
`;
|
||||
|
||||
export default function Deduplicate() {
|
||||
const { setDialogMessage, startLoading, finishLoading, showNavBar } =
|
||||
useContext(AppContext);
|
||||
const { showNavBar, showLoadingBar, hideLoadingBar, setDialogMessage } =
|
||||
useAppContext();
|
||||
const [duplicates, setDuplicates] = useState<Duplicate[]>(null);
|
||||
const [collectionNameMap, setCollectionNameMap] = useState(
|
||||
new Map<number, string>(),
|
||||
@@ -70,42 +70,48 @@ export default function Deduplicate() {
|
||||
}, []);
|
||||
|
||||
const syncWithRemote = async () => {
|
||||
startLoading();
|
||||
const collections = await getLocalCollections();
|
||||
const collectionNameMap = new Map<number, string>();
|
||||
for (const collection of collections) {
|
||||
collectionNameMap.set(collection.id, collection.name);
|
||||
}
|
||||
setCollectionNameMap(collectionNameMap);
|
||||
const files = await getLocalFiles();
|
||||
const duplicateFiles = await getDuplicates(files, collectionNameMap);
|
||||
const currFileSizeMap = new Map<number, number>();
|
||||
let toSelectFileIDs: number[] = [];
|
||||
let count = 0;
|
||||
for (const dupe of duplicateFiles) {
|
||||
// select all except first file
|
||||
toSelectFileIDs = [
|
||||
...toSelectFileIDs,
|
||||
...dupe.files.slice(1).map((f) => f.id),
|
||||
];
|
||||
count += dupe.files.length - 1;
|
||||
|
||||
for (const file of dupe.files) {
|
||||
currFileSizeMap.set(file.id, dupe.size);
|
||||
showLoadingBar();
|
||||
try {
|
||||
const collections = await getLocalCollections();
|
||||
const collectionNameMap = new Map<number, string>();
|
||||
for (const collection of collections) {
|
||||
collectionNameMap.set(collection.id, collection.name);
|
||||
}
|
||||
setCollectionNameMap(collectionNameMap);
|
||||
const files = await getLocalFiles();
|
||||
const duplicateFiles = await getDuplicates(
|
||||
files,
|
||||
collectionNameMap,
|
||||
);
|
||||
const currFileSizeMap = new Map<number, number>();
|
||||
let toSelectFileIDs: number[] = [];
|
||||
let count = 0;
|
||||
for (const dupe of duplicateFiles) {
|
||||
// select all except first file
|
||||
toSelectFileIDs = [
|
||||
...toSelectFileIDs,
|
||||
...dupe.files.slice(1).map((f) => f.id),
|
||||
];
|
||||
count += dupe.files.length - 1;
|
||||
|
||||
for (const file of dupe.files) {
|
||||
currFileSizeMap.set(file.id, dupe.size);
|
||||
}
|
||||
}
|
||||
setDuplicates(duplicateFiles);
|
||||
const selectedFiles = {
|
||||
count: count,
|
||||
ownCount: count,
|
||||
collectionID: ALL_SECTION,
|
||||
context: undefined,
|
||||
};
|
||||
for (const fileID of toSelectFileIDs) {
|
||||
selectedFiles[fileID] = true;
|
||||
}
|
||||
setSelected(selectedFiles);
|
||||
} finally {
|
||||
hideLoadingBar();
|
||||
}
|
||||
setDuplicates(duplicateFiles);
|
||||
const selectedFiles = {
|
||||
count: count,
|
||||
ownCount: count,
|
||||
collectionID: ALL_SECTION,
|
||||
context: undefined,
|
||||
};
|
||||
for (const fileID of toSelectFileIDs) {
|
||||
selectedFiles[fileID] = true;
|
||||
}
|
||||
setSelected(selectedFiles);
|
||||
finishLoading();
|
||||
};
|
||||
|
||||
const duplicateFiles = useMemoSingleThreaded(() => {
|
||||
@@ -120,7 +126,7 @@ export default function Deduplicate() {
|
||||
|
||||
const deleteFileHelper = async () => {
|
||||
try {
|
||||
startLoading();
|
||||
showLoadingBar();
|
||||
const selectedFiles = getSelectedFiles(selected, duplicateFiles);
|
||||
await trashFiles(selectedFiles);
|
||||
|
||||
@@ -160,7 +166,7 @@ export default function Deduplicate() {
|
||||
}
|
||||
} finally {
|
||||
await syncWithRemote();
|
||||
finishLoading();
|
||||
hideLoadingBar();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -43,7 +43,8 @@ import {
|
||||
setSearchCollectionsAndFiles,
|
||||
} from "@/new/photos/services/search";
|
||||
import type { SearchOption } from "@/new/photos/services/search/types";
|
||||
import { AppContext } from "@/new/photos/types/context";
|
||||
import { initSettings } from "@/new/photos/services/settings";
|
||||
import { useAppContext } from "@/new/photos/types/context";
|
||||
import { splitByPredicate } from "@/utils/array";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import {
|
||||
@@ -100,14 +101,7 @@ import PlanSelector from "components/pages/gallery/PlanSelector";
|
||||
import SelectedFileOptions from "components/pages/gallery/SelectedFileOptions";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { createContext, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import {
|
||||
constructEmailList,
|
||||
@@ -231,12 +225,12 @@ export default function Gallery() {
|
||||
const resync = useRef<{ force: boolean; silent: boolean }>();
|
||||
|
||||
const {
|
||||
startLoading,
|
||||
finishLoading,
|
||||
showLoadingBar,
|
||||
hideLoadingBar,
|
||||
setDialogMessage,
|
||||
logout,
|
||||
...appContext
|
||||
} = useContext(AppContext);
|
||||
} = useAppContext();
|
||||
const [userIDToEmailMap, setUserIDToEmailMap] =
|
||||
useState<Map<number, string>>(null);
|
||||
const [emailList, setEmailList] = useState<string[]>(null);
|
||||
@@ -348,6 +342,7 @@ export default function Gallery() {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
initSettings();
|
||||
await downloadManager.init(token);
|
||||
setupSelectAllKeyBoardShortcutHandler();
|
||||
dispatch({ type: "showAll" });
|
||||
@@ -574,7 +569,7 @@ export default function Gallery() {
|
||||
if (!tokenValid) {
|
||||
throw new Error(CustomError.SESSION_EXPIRED);
|
||||
}
|
||||
!silent && startLoading();
|
||||
!silent && showLoadingBar();
|
||||
await preFileInfoSync();
|
||||
const allCollections = await getAllLatestCollections();
|
||||
const [hiddenCollections, collections] = splitByPredicate(
|
||||
@@ -626,7 +621,7 @@ export default function Gallery() {
|
||||
} finally {
|
||||
dispatch({ type: "clearTempDeleted" });
|
||||
dispatch({ type: "clearTempHidden" });
|
||||
!silent && finishLoading();
|
||||
!silent && hideLoadingBar();
|
||||
}
|
||||
syncInProgress.current = false;
|
||||
if (resync.current) {
|
||||
@@ -690,7 +685,7 @@ export default function Gallery() {
|
||||
|
||||
const collectionOpsHelper =
|
||||
(ops: COLLECTION_OPS_TYPE) => async (collection: Collection) => {
|
||||
startLoading();
|
||||
showLoadingBar();
|
||||
try {
|
||||
setOpenCollectionSelector(false);
|
||||
const selectedFiles = getSelectedFiles(selected, filteredFiles);
|
||||
@@ -719,12 +714,12 @@ export default function Gallery() {
|
||||
content: t("generic_error_retry"),
|
||||
});
|
||||
} finally {
|
||||
finishLoading();
|
||||
hideLoadingBar();
|
||||
}
|
||||
};
|
||||
|
||||
const fileOpsHelper = (ops: FILE_OPS_TYPE) => async () => {
|
||||
startLoading();
|
||||
showLoadingBar();
|
||||
try {
|
||||
// passing files here instead of filteredData for hide ops because we want to move all files copies to hidden collection
|
||||
const selectedFiles = getSelectedFiles(
|
||||
@@ -758,14 +753,14 @@ export default function Gallery() {
|
||||
content: t("generic_error_retry"),
|
||||
});
|
||||
} finally {
|
||||
finishLoading();
|
||||
hideLoadingBar();
|
||||
}
|
||||
};
|
||||
|
||||
const showCreateCollectionModal = (ops: COLLECTION_OPS_TYPE) => {
|
||||
const callback = async (collectionName: string) => {
|
||||
try {
|
||||
startLoading();
|
||||
showLoadingBar();
|
||||
const collection = await createAlbum(collectionName);
|
||||
await collectionOpsHelper(ops)(collection);
|
||||
} catch (e) {
|
||||
@@ -777,7 +772,7 @@ export default function Gallery() {
|
||||
content: t("generic_error_retry"),
|
||||
});
|
||||
} finally {
|
||||
finishLoading();
|
||||
hideLoadingBar();
|
||||
}
|
||||
};
|
||||
return () =>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "@/new/photos/services/collection";
|
||||
import downloadManager from "@/new/photos/services/download";
|
||||
import { sortFiles } from "@/new/photos/services/files";
|
||||
import { AppContext } from "@/new/photos/types/context";
|
||||
import { useAppContext } from "@/new/photos/types/context";
|
||||
import {
|
||||
CenteredFlex,
|
||||
FluidContainer,
|
||||
@@ -54,7 +54,7 @@ import Uploader from "components/Upload/Uploader";
|
||||
import { UploadSelectorInputs } from "components/UploadSelectorInputs";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import {
|
||||
getLocalPublicCollection,
|
||||
@@ -90,7 +90,8 @@ export default function PublicCollectionGallery() {
|
||||
const [publicFiles, setPublicFiles] = useState<EnteFile[]>(null);
|
||||
const [publicCollection, setPublicCollection] = useState<Collection>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string>(null);
|
||||
const appContext = useContext(AppContext);
|
||||
const { showLoadingBar, hideLoadingBar, setDialogMessage } =
|
||||
useAppContext();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const router = useRouter();
|
||||
const [isPasswordProtected, setIsPasswordProtected] =
|
||||
@@ -185,7 +186,7 @@ export default function PublicCollectionGallery() {
|
||||
};
|
||||
|
||||
const showPublicLinkExpiredMessage = () =>
|
||||
appContext.setDialogMessage({
|
||||
setDialogMessage({
|
||||
title: t("LINK_EXPIRED"),
|
||||
content: t("LINK_EXPIRED_MESSAGE"),
|
||||
|
||||
@@ -316,7 +317,7 @@ export default function PublicCollectionGallery() {
|
||||
const syncWithRemote = async () => {
|
||||
const collectionUID = getPublicCollectionUID(token.current);
|
||||
try {
|
||||
appContext.startLoading();
|
||||
showLoadingBar();
|
||||
setLoading(true);
|
||||
const [collection, userReferralCode] = await getPublicCollection(
|
||||
token.current,
|
||||
@@ -381,7 +382,7 @@ export default function PublicCollectionGallery() {
|
||||
log.error("failed to sync public album with remote", e);
|
||||
}
|
||||
} finally {
|
||||
appContext.finishLoading();
|
||||
hideLoadingBar();
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
@@ -427,7 +428,7 @@ export default function PublicCollectionGallery() {
|
||||
throw e;
|
||||
}
|
||||
await syncWithRemote();
|
||||
appContext.finishLoading();
|
||||
hideLoadingBar();
|
||||
} catch (e) {
|
||||
log.error("failed to verifyLinkPassword", e);
|
||||
setFieldError(`${t("generic_error_retry")} ${e.message}`);
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
getCollectionUserFacingName,
|
||||
} from "@/new/photos/services/collection";
|
||||
import downloadManager from "@/new/photos/services/download";
|
||||
import { updateExifIfNeededAndPossible } from "@/new/photos/services/exif-update";
|
||||
import {
|
||||
exportMetadataDirectoryName,
|
||||
exportTrashDirectoryName,
|
||||
@@ -939,16 +938,12 @@ class ExportService {
|
||||
try {
|
||||
const fileUID = getExportRecordFileUID(file);
|
||||
const originalFileStream = await downloadManager.getFile(file);
|
||||
const updatedFileStream = await updateExifIfNeededAndPossible(
|
||||
file,
|
||||
originalFileStream,
|
||||
);
|
||||
if (file.metadata.fileType === FileType.livePhoto) {
|
||||
await this.exportLivePhoto(
|
||||
exportDir,
|
||||
fileUID,
|
||||
collectionExportPath,
|
||||
updatedFileStream,
|
||||
originalFileStream,
|
||||
file,
|
||||
);
|
||||
} else {
|
||||
@@ -965,7 +960,7 @@ class ExportService {
|
||||
await writeStream(
|
||||
electron,
|
||||
`${collectionExportPath}/${fileExportName}`,
|
||||
updatedFileStream,
|
||||
originalFileStream,
|
||||
);
|
||||
await this.addFileExportedRecord(
|
||||
exportDir,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { accountLogout } from "@/accounts/services/logout";
|
||||
import log from "@/base/log";
|
||||
import DownloadManager from "@/new/photos/services/download";
|
||||
import { clearFeatureFlagSessionState } from "@/new/photos/services/feature-flags";
|
||||
import { logoutML, terminateMLWorker } from "@/new/photos/services/ml";
|
||||
import { logoutSearch } from "@/new/photos/services/search";
|
||||
import { logoutSettings } from "@/new/photos/services/settings";
|
||||
import exportService from "./export";
|
||||
|
||||
/**
|
||||
@@ -37,9 +37,9 @@ export const photosLogout = async () => {
|
||||
log.info("logout (photos)");
|
||||
|
||||
try {
|
||||
clearFeatureFlagSessionState();
|
||||
logoutSettings();
|
||||
} catch (e) {
|
||||
ignoreError("feature-flag", e);
|
||||
ignoreError("settings", e);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { triggerFeatureFlagsFetchIfNeeded } from "@/new/photos/services/feature-flags";
|
||||
import { isMLSupported, mlStatusSync, mlSync } from "@/new/photos/services/ml";
|
||||
import { searchDataSync } from "@/new/photos/services/search";
|
||||
import { triggerSettingsSyncIfNeeded } from "@/new/photos/services/settings";
|
||||
import { syncMapEnabled } from "services/userService";
|
||||
|
||||
/**
|
||||
* Part 1 of {@link sync}. See TODO below for why this is split.
|
||||
*/
|
||||
export const preFileInfoSync = async () => {
|
||||
triggerFeatureFlagsFetchIfNeeded();
|
||||
triggerSettingsSyncIfNeeded();
|
||||
await Promise.all([isMLSupported && mlStatusSync()]);
|
||||
};
|
||||
|
||||
|
||||
@@ -124,47 +124,3 @@ body {
|
||||
.pswp__caption--empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bg-upload-progress-bar {
|
||||
background-color: #51cd7c;
|
||||
}
|
||||
|
||||
.carousel-inner {
|
||||
padding-bottom: 50px !important;
|
||||
}
|
||||
|
||||
.carousel-indicators li {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.carousel-indicators .active {
|
||||
background-color: #51cd7c;
|
||||
}
|
||||
|
||||
div.otp-input input {
|
||||
width: 36px !important;
|
||||
height: 36px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
div.otp-input input::placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
div.otp-input input:not(:placeholder-shown),
|
||||
div.otp-input input:focus {
|
||||
border: 2px solid #51cd7c;
|
||||
border-radius: 1px;
|
||||
-webkit-transition: 0.5s;
|
||||
transition: 0.5s;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.flash-message {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import { ItemVisibility } from "@/media/file-metadata";
|
||||
import { FileType } from "@/media/file-type";
|
||||
import { decodeLivePhoto } from "@/media/live-photo";
|
||||
import DownloadManager from "@/new/photos/services/download";
|
||||
import { updateExifIfNeededAndPossible } from "@/new/photos/services/exif-update";
|
||||
import {
|
||||
isArchivedFile,
|
||||
updateMagicMetadata,
|
||||
@@ -79,9 +78,6 @@ export async function downloadFile(file: EnteFile) {
|
||||
const fileType = await detectFileTypeInfo(
|
||||
new File([fileBlob], file.metadata.title),
|
||||
);
|
||||
fileBlob = await new Response(
|
||||
await updateExifIfNeededAndPossible(file, fileBlob.stream()),
|
||||
).blob();
|
||||
fileBlob = new Blob([fileBlob], { type: fileType.mimeType });
|
||||
const tempURL = URL.createObjectURL(fileBlob);
|
||||
downloadAndRevokeObjectURL(tempURL, file.metadata.title);
|
||||
@@ -397,10 +393,9 @@ async function downloadFileDesktop(
|
||||
const fs = electron.fs;
|
||||
|
||||
const stream = await DownloadManager.getFile(file);
|
||||
const updatedStream = await updateExifIfNeededAndPossible(file, stream);
|
||||
|
||||
if (file.metadata.fileType === FileType.livePhoto) {
|
||||
const fileBlob = await new Response(updatedStream).blob();
|
||||
const fileBlob = await new Response(stream).blob();
|
||||
const { imageFileName, imageData, videoFileName, videoData } =
|
||||
await decodeLivePhoto(file.metadata.title, fileBlob);
|
||||
const imageExportName = await safeFileName(
|
||||
@@ -436,11 +431,7 @@ async function downloadFileDesktop(
|
||||
file.metadata.title,
|
||||
fs.exists,
|
||||
);
|
||||
await writeStream(
|
||||
electron,
|
||||
`${downloadDir}/${fileExportName}`,
|
||||
updatedStream,
|
||||
);
|
||||
await writeStream(electron, `${downloadDir}/${fileExportName}`, stream);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ export const getTrashFilesMessage = (
|
||||
action: deleteFileHelper,
|
||||
text: t("MOVE_TO_TRASH"),
|
||||
variant: "critical",
|
||||
autoFocus: true,
|
||||
},
|
||||
close: { text: t("cancel") },
|
||||
});
|
||||
|
||||
@@ -132,7 +132,7 @@ with Next.js.
|
||||
|
||||
For more details, see [translations.md](translations.md).
|
||||
|
||||
### Others
|
||||
### Other UI components
|
||||
|
||||
- [formik](https://github.com/jaredpalmer/formik) provides an easier to use
|
||||
abstraction for dealing with form state, validation and submission states
|
||||
@@ -140,6 +140,9 @@ For more details, see [translations.md](translations.md).
|
||||
|
||||
- [react-select](https://react-select.com/) is used for search dropdowns.
|
||||
|
||||
- [react-otp-input](https://github.com/devfolioco/react-otp-input) is used to
|
||||
render a segmented OTP input field for 2FA authentication.
|
||||
|
||||
## Utilities
|
||||
|
||||
- [comlink](https://github.com/GoogleChromeLabs/comlink) provides a minimal
|
||||
@@ -164,8 +167,7 @@ For more details, see [translations.md](translations.md).
|
||||
## Media
|
||||
|
||||
- [ExifReader](https://github.com/mattiasw/ExifReader) is used for Exif
|
||||
parsing. [piexifjs](https://github.com/hMatoba/piexifjs) is used for writing
|
||||
back Exif (only supports JPEG).
|
||||
parsing.
|
||||
|
||||
- [jszip](https://github.com/Stuk/jszip) is used for reading zip files in the
|
||||
web code (Live photos are zip files under the hood). Note that the desktop
|
||||
@@ -181,8 +183,6 @@ For more details, see [translations.md](translations.md).
|
||||
|
||||
## Photos app specific
|
||||
|
||||
### General
|
||||
|
||||
- [react-dropzone](https://github.com/react-dropzone/react-dropzone/) is a
|
||||
React hook to create a drag-and-drop input zone.
|
||||
|
||||
@@ -193,13 +193,21 @@ For more details, see [translations.md](translations.md).
|
||||
- [chrono-node](https://github.com/wanasit/chrono) is used for parsing natural
|
||||
language queries into dates for showing search results.
|
||||
|
||||
### Face search
|
||||
|
||||
- [matrix](https://github.com/mljs/matrix) is mathematical matrix abstraction.
|
||||
It is used alongwith
|
||||
- [matrix](https://github.com/mljs/matrix) is mathematical matrix abstraction
|
||||
by the machine learning code. It is used alongwith
|
||||
[similarity-transformation](https://github.com/shaileshpandit/similarity-transformation-js)
|
||||
during face alignment.
|
||||
|
||||
### UI
|
||||
|
||||
- [react-top-loading-bar](https://github.com/klendi/react-top-loading-bar) is
|
||||
used for showing a progress indicator for global actions (This shouldn't be
|
||||
used always, it is only meant as a fallback when there isn't an otherwise
|
||||
suitable place for showing a local activity indicator).
|
||||
|
||||
- [pure-react-carousel](https://github.com/express-labs/pure-react-carousel)
|
||||
is used for the feature carousel on the welcome (login / signup) screen.
|
||||
|
||||
## Auth app specific
|
||||
|
||||
- [otpauth](https://github.com/hectorm/otpauth) is used for the generation of
|
||||
|
||||
@@ -4,10 +4,10 @@ import {
|
||||
CenteredFlex,
|
||||
VerticallyCentered,
|
||||
} from "@ente/shared/components/Container";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { Box, Typography, styled } from "@mui/material";
|
||||
import { Formik, type FormikHelpers } from "formik";
|
||||
import { t } from "i18next";
|
||||
import { useRef, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import OtpInput from "react-otp-input";
|
||||
|
||||
interface formValues {
|
||||
@@ -25,7 +25,7 @@ export type VerifyTwoFactorCallback = (
|
||||
|
||||
export default function VerifyTwoFactor(props: Props) {
|
||||
const [waiting, setWaiting] = useState(false);
|
||||
const otpInputRef = useRef<OtpInput>(null);
|
||||
const [shouldAutoFocus, setShouldAutoFocus] = useState(true);
|
||||
|
||||
const markSuccessful = async () => {
|
||||
setWaiting(false);
|
||||
@@ -40,11 +40,13 @@ export default function VerifyTwoFactor(props: Props) {
|
||||
await props.onSubmit(otp, markSuccessful);
|
||||
} catch (e) {
|
||||
resetForm();
|
||||
for (let i = 0; i < 6; i++) {
|
||||
otpInputRef.current?.focusPrevInput();
|
||||
}
|
||||
const message = e instanceof Error ? e.message : "";
|
||||
setFieldError("otp", `${t("generic_error_retry")} ${message}`);
|
||||
// Workaround (toggling shouldAutoFocus) to reset the focus back to
|
||||
// the first input field in case of errors.
|
||||
// https://github.com/devfolioco/react-otp-input/issues/420
|
||||
setShouldAutoFocus(false);
|
||||
setTimeout(() => setShouldAutoFocus(true), 100);
|
||||
}
|
||||
setWaiting(false);
|
||||
};
|
||||
@@ -71,17 +73,17 @@ export default function VerifyTwoFactor(props: Props) {
|
||||
</Typography>
|
||||
<Box my={2}>
|
||||
<OtpInput
|
||||
ref={otpInputRef}
|
||||
shouldAutoFocus
|
||||
shouldAutoFocus={shouldAutoFocus}
|
||||
value={values.otp}
|
||||
onChange={onChange(
|
||||
handleChange("otp"),
|
||||
submitForm,
|
||||
)}
|
||||
numInputs={6}
|
||||
separator={"-"}
|
||||
isInputNum
|
||||
className={"otp-input"}
|
||||
renderSeparator={<span>-</span>}
|
||||
renderInput={(props) => (
|
||||
<IndividualInput {...props} />
|
||||
)}
|
||||
/>
|
||||
{errors.otp && (
|
||||
<CenteredFlex sx={{ mt: 1 }}>
|
||||
@@ -107,3 +109,23 @@ export default function VerifyTwoFactor(props: Props) {
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
const IndividualInput = styled("input")(
|
||||
({ theme }) => `
|
||||
font-size: 1.5rem;
|
||||
padding: 4px;
|
||||
width: 40px !important;
|
||||
aspect-ratio: 1;
|
||||
margin-inline: 8px;
|
||||
border: 1px solid ${theme.colors.accent.A700};
|
||||
border-radius: 1px;
|
||||
outline-color: ${theme.colors.accent.A300};
|
||||
transition: 0.5s;
|
||||
|
||||
${theme.breakpoints.down("sm")} {
|
||||
font-size: 1rem;
|
||||
padding: 4px;
|
||||
width: 32px !important;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"dependencies": {
|
||||
"@/base": "*",
|
||||
"@ente/eslint-config": "*",
|
||||
"@ente/shared": "*"
|
||||
"@ente/shared": "*",
|
||||
"react-otp-input": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Drawer, styled } from "@mui/material";
|
||||
|
||||
export const EnteDrawer = styled(Drawer)(({ theme }) => ({
|
||||
"& .MuiPaper-root": {
|
||||
maxWidth: "375px",
|
||||
width: "100%",
|
||||
scrollbarWidth: "thin",
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
17
web/packages/base/components/mui/SidebarDrawer.tsx
Normal file
17
web/packages/base/components/mui/SidebarDrawer.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Drawer, styled } from "@mui/material";
|
||||
|
||||
/**
|
||||
* A MUI {@link Drawer} with a standard set of styling that we use for our left
|
||||
* and right sidebar panels.
|
||||
*
|
||||
* It is width limited to 375px, and always at full width. It also has a default
|
||||
* padding.
|
||||
*/
|
||||
export const SidebarDrawer = styled(Drawer)(({ theme }) => ({
|
||||
"& .MuiPaper-root": {
|
||||
maxWidth: "375px",
|
||||
width: "100%",
|
||||
scrollbarWidth: "thin",
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
@@ -4,18 +4,18 @@
|
||||
"intro_slide_2_title": "",
|
||||
"intro_slide_2": "",
|
||||
"intro_slide_3_title": "",
|
||||
"intro_slide_3": "",
|
||||
"intro_slide_3": "Android, iOS, Інтэрнэт, Камп’ютар",
|
||||
"login": "Увайсці",
|
||||
"sign_up": "Рэгістрацыя",
|
||||
"NEW_USER": "",
|
||||
"EXISTING_USER": "",
|
||||
"enter_name": "",
|
||||
"NEW_USER": "Новы ў Ente",
|
||||
"EXISTING_USER": "Існуючы карыстальнік",
|
||||
"enter_name": "Увядзіце імя",
|
||||
"PUBLIC_UPLOADER_NAME_MESSAGE": "",
|
||||
"ENTER_EMAIL": "",
|
||||
"EMAIL_ERROR": "",
|
||||
"required": "",
|
||||
"ENTER_EMAIL": "Увядзіце адрас электроннай пошты",
|
||||
"EMAIL_ERROR": "Увядзіце сапраўдны адрас электроннай пошты",
|
||||
"required": "патрабуецца",
|
||||
"EMAIL_SENT": "",
|
||||
"CHECK_INBOX": "",
|
||||
"CHECK_INBOX": "Праверце свае ўваходныя лісты (і спам) для завяршэння праверкі",
|
||||
"ENTER_OTT": "Код пацвярджэння",
|
||||
"RESEND_MAIL": "Паўторна адправіць код",
|
||||
"VERIFY": "",
|
||||
@@ -28,14 +28,14 @@
|
||||
"password": "Пароль",
|
||||
"link_password_description": "",
|
||||
"unlock": "Разблакіраваць",
|
||||
"SET_PASSPHRASE": "",
|
||||
"SET_PASSPHRASE": "Задаць пароль",
|
||||
"VERIFY_PASSPHRASE": "Увайсці",
|
||||
"INCORRECT_PASSPHRASE": "Няправільны пароль",
|
||||
"ENTER_ENC_PASSPHRASE": "",
|
||||
"PASSPHRASE_DISCLAIMER": "",
|
||||
"key_generation_in_progress": "",
|
||||
"PASSPHRASE_HINT": "Пароль",
|
||||
"CONFIRM_PASSPHRASE": "",
|
||||
"CONFIRM_PASSPHRASE": "Пацвердзіць пароль",
|
||||
"REFERRAL_CODE_HINT": "",
|
||||
"REFERRAL_INFO": "",
|
||||
"PASSPHRASE_MATCH_ERROR": "",
|
||||
@@ -45,17 +45,17 @@
|
||||
"create_albums": "",
|
||||
"enter_album_name": "",
|
||||
"close_key": "",
|
||||
"enter_file_name": "",
|
||||
"enter_file_name": "Назва файла",
|
||||
"close": "Закрыць",
|
||||
"no": "Не",
|
||||
"nothing_here": "",
|
||||
"upload": "",
|
||||
"import": "",
|
||||
"add_photos": "",
|
||||
"add_more_photos": "",
|
||||
"upload": "Запампаваць",
|
||||
"import": "Імпартаваць",
|
||||
"add_photos": "Дадаць фота",
|
||||
"add_more_photos": "Дадаць больш фота",
|
||||
"add_photos_count_one": "",
|
||||
"add_photos_count": "",
|
||||
"select_photos": "",
|
||||
"select_photos": "Абраць фота",
|
||||
"FILE_UPLOAD": "",
|
||||
"UPLOAD_STAGE_MESSAGE": {
|
||||
"0": "",
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
"intro_slide_3": "„Android“, „iOS“, internete, darbalaukyje",
|
||||
"login": "Prisijungti",
|
||||
"sign_up": "Registruotis",
|
||||
"NEW_USER": "Naujas platformoje „Ente“",
|
||||
"NEW_USER": "Naujas sistemoje „Ente“",
|
||||
"EXISTING_USER": "Esamas naudotojas",
|
||||
"enter_name": "Įveskite vardą",
|
||||
"PUBLIC_UPLOADER_NAME_MESSAGE": "Pridėkite vardą, kad draugai žinotų, kam padėkoti už šias puikias nuotraukas.",
|
||||
"ENTER_EMAIL": "Įveskite el. pašto adresą",
|
||||
"EMAIL_ERROR": "Įveskite tinkamą el. paštą.",
|
||||
"required": "Privaloma",
|
||||
"EMAIL_SENT": "Patvirtinimo kodas išsiųstas į <a>{{email}}</a>",
|
||||
"EMAIL_SENT": "Patvirtinimo kodas išsiųstas adresu <a>{{email}}</a>",
|
||||
"CHECK_INBOX": "Patikrinkite savo gautieją (ir šlamštą), kad užbaigtumėte patvirtinimą",
|
||||
"ENTER_OTT": "Patvirtinimo kodas",
|
||||
"RESEND_MAIL": "Siųsti kodą iš naujo",
|
||||
|
||||
@@ -222,8 +222,8 @@
|
||||
"photos_count": "{{count, number}} memórias",
|
||||
"terms_and_conditions": "Eu concordo com os <u-terms>termos de serviço</u-terms> e <u-policy>política de privacidade</u-policy>",
|
||||
"SELECTED": "selecionado",
|
||||
"people": "",
|
||||
"indexing_scheduled": "",
|
||||
"people": "Pessoas",
|
||||
"indexing_scheduled": "Indexação está programada...",
|
||||
"indexing_photos": "Indexar fotos ({{nSyncedFiles, number}} / {{nTotalFiles, number}})",
|
||||
"indexing_fetching": "Obtendo índices ({{nSyncedFiles, number}} / {{nTotalFiles, number}})",
|
||||
"indexing_people": "Indexar pessoas em {{nSyncedFiles, number}} fotos...",
|
||||
@@ -288,312 +288,312 @@
|
||||
"ETAGS_BLOCKED": "<p>Não foi possível fazer o envio dos seguintes arquivos devido à configuração do seu navegador.</p><p>Por favor, desative quaisquer complementos que possam estar impedindo o ente de utilizar <code>eTags</code> para enviar arquivos grandes, ou utilize nosso <a>aplicativo para computador</a> para uma experiência de importação mais confiável.</p>",
|
||||
"LIVE_PHOTOS_DETECTED": "Os ficheiros de fotografia e vídeo das suas Live Photos foram fundidos num único ficheiro",
|
||||
"RETRY_FAILED": "Repetir envios com falha",
|
||||
"FAILED_UPLOADS": "",
|
||||
"failed_uploads_hint": "",
|
||||
"SKIPPED_FILES": "",
|
||||
"THUMBNAIL_GENERATION_FAILED_UPLOADS": "",
|
||||
"UNSUPPORTED_FILES": "",
|
||||
"SUCCESSFUL_UPLOADS": "",
|
||||
"SKIPPED_INFO": "",
|
||||
"UNSUPPORTED_INFO": "",
|
||||
"BLOCKED_UPLOADS": "",
|
||||
"INPROGRESS_METADATA_EXTRACTION": "",
|
||||
"INPROGRESS_UPLOADS": "",
|
||||
"TOO_LARGE_UPLOADS": "",
|
||||
"LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "",
|
||||
"LARGER_THAN_AVAILABLE_STORAGE_INFO": "",
|
||||
"TOO_LARGE_INFO": "",
|
||||
"THUMBNAIL_GENERATION_FAILED_INFO": "",
|
||||
"upload_to_album": "",
|
||||
"add_to_album": "",
|
||||
"move_to_album": "",
|
||||
"unhide_to_album": "",
|
||||
"restore_to_album": "",
|
||||
"section_all": "",
|
||||
"section_uncategorized": "",
|
||||
"section_archive": "",
|
||||
"section_hidden": "",
|
||||
"section_trash": "",
|
||||
"favorites": "",
|
||||
"archive": "",
|
||||
"archive_album": "",
|
||||
"unarchive": "",
|
||||
"unarchive_album": "",
|
||||
"hide_collection": "",
|
||||
"unhide_collection": "",
|
||||
"MOVE": "",
|
||||
"add": "",
|
||||
"REMOVE": "",
|
||||
"YES_REMOVE": "",
|
||||
"REMOVE_FROM_COLLECTION": "",
|
||||
"MOVE_TO_TRASH": "",
|
||||
"TRASH_FILES_MESSAGE": "",
|
||||
"TRASH_FILE_MESSAGE": "",
|
||||
"DELETE_PERMANENTLY": "",
|
||||
"RESTORE": "",
|
||||
"empty_trash": "",
|
||||
"empty_trash_title": "",
|
||||
"empty_trash_message": "",
|
||||
"leave_album": "",
|
||||
"leave_shared_album_title": "",
|
||||
"leave_shared_album_message": "",
|
||||
"leave_shared_album": "",
|
||||
"NOT_FILE_OWNER": "",
|
||||
"CONFIRM_SELF_REMOVE_MESSAGE": "",
|
||||
"CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "",
|
||||
"sort_by_creation_time_ascending": "",
|
||||
"sort_by_updation_time_descending": "",
|
||||
"sort_by_name": "",
|
||||
"FIX_CREATION_TIME": "",
|
||||
"FIX_CREATION_TIME_IN_PROGRESS": "",
|
||||
"CREATION_TIME_UPDATED": "",
|
||||
"UPDATE_CREATION_TIME_NOT_STARTED": "",
|
||||
"UPDATE_CREATION_TIME_COMPLETED": "",
|
||||
"UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "",
|
||||
"CAPTION_CHARACTER_LIMIT": "",
|
||||
"DATE_TIME_ORIGINAL": "",
|
||||
"DATE_TIME_DIGITIZED": "",
|
||||
"METADATA_DATE": "",
|
||||
"CUSTOM_TIME": "",
|
||||
"sharing_details": "",
|
||||
"modify_sharing": "",
|
||||
"ADD_COLLABORATORS": "",
|
||||
"ADD_NEW_EMAIL": "",
|
||||
"shared_with_people_count_zero": "",
|
||||
"shared_with_people_count_one": "",
|
||||
"shared_with_people_count": "",
|
||||
"participants_count_zero": "",
|
||||
"participants_count_one": "",
|
||||
"participants_count": "",
|
||||
"ADD_VIEWERS": "",
|
||||
"CHANGE_PERMISSIONS_TO_VIEWER": "",
|
||||
"CHANGE_PERMISSIONS_TO_COLLABORATOR": "",
|
||||
"CONVERT_TO_VIEWER": "",
|
||||
"CONVERT_TO_COLLABORATOR": "",
|
||||
"CHANGE_PERMISSION": "",
|
||||
"REMOVE_PARTICIPANT": "",
|
||||
"CONFIRM_REMOVE": "",
|
||||
"MANAGE": "",
|
||||
"ADDED_AS": "",
|
||||
"COLLABORATOR_RIGHTS": "",
|
||||
"REMOVE_PARTICIPANT_HEAD": "",
|
||||
"OWNER": "",
|
||||
"COLLABORATORS": "",
|
||||
"ADD_MORE": "",
|
||||
"VIEWERS": "",
|
||||
"OR_ADD_EXISTING": "",
|
||||
"REMOVE_PARTICIPANT_MESSAGE": "",
|
||||
"NOT_FOUND": "",
|
||||
"LINK_EXPIRED": "",
|
||||
"LINK_EXPIRED_MESSAGE": "",
|
||||
"MANAGE_LINK": "",
|
||||
"LINK_TOO_MANY_REQUESTS": "",
|
||||
"FILE_DOWNLOAD": "",
|
||||
"link_password_lock": "",
|
||||
"PUBLIC_COLLECT": "",
|
||||
"LINK_DEVICE_LIMIT": "",
|
||||
"NO_DEVICE_LIMIT": "",
|
||||
"LINK_EXPIRY": "",
|
||||
"NEVER": "",
|
||||
"DISABLE_FILE_DOWNLOAD": "",
|
||||
"DISABLE_FILE_DOWNLOAD_MESSAGE": "",
|
||||
"SHARED_USING": "",
|
||||
"SHARING_REFERRAL_CODE": "",
|
||||
"LIVE": "",
|
||||
"DISABLE_PASSWORD": "",
|
||||
"DISABLE_PASSWORD_MESSAGE": "",
|
||||
"PASSWORD_LOCK": "",
|
||||
"LOCK": "",
|
||||
"file": "",
|
||||
"folder": "",
|
||||
"google_takeout": "",
|
||||
"DEDUPLICATE_FILES": "",
|
||||
"NO_DUPLICATES_FOUND": "",
|
||||
"FILES": "",
|
||||
"EACH": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "",
|
||||
"STOP_UPLOADS_HEADER": "",
|
||||
"YES_STOP_UPLOADS": "",
|
||||
"STOP_DOWNLOADS_HEADER": "",
|
||||
"YES_STOP_DOWNLOADS": "",
|
||||
"STOP_ALL_DOWNLOADS_MESSAGE": "",
|
||||
"albums": "",
|
||||
"albums_count_one": "",
|
||||
"albums_count": "",
|
||||
"all_albums": "",
|
||||
"all_hidden_albums": "",
|
||||
"hidden_albums": "",
|
||||
"hidden_items": "",
|
||||
"ENTER_TWO_FACTOR_OTP": "",
|
||||
"create_account": "",
|
||||
"COPIED": "",
|
||||
"WATCH_FOLDERS": "",
|
||||
"upgrade_now": "",
|
||||
"renew_now": "",
|
||||
"STORAGE": "",
|
||||
"USED": "",
|
||||
"YOU": "",
|
||||
"FAMILY": "",
|
||||
"FREE": "",
|
||||
"OF": "",
|
||||
"WATCHED_FOLDERS": "",
|
||||
"NO_FOLDERS_ADDED": "",
|
||||
"FOLDERS_AUTOMATICALLY_MONITORED": "",
|
||||
"UPLOAD_NEW_FILES_TO_ENTE": "",
|
||||
"REMOVE_DELETED_FILES_FROM_ENTE": "",
|
||||
"ADD_FOLDER": "",
|
||||
"STOP_WATCHING": "",
|
||||
"STOP_WATCHING_FOLDER": "",
|
||||
"STOP_WATCHING_DIALOG_MESSAGE": "",
|
||||
"YES_STOP": "",
|
||||
"CHANGE_FOLDER": "",
|
||||
"FAMILY_PLAN": "",
|
||||
"debug_logs": "",
|
||||
"download_logs": "",
|
||||
"download_logs_message": "",
|
||||
"WEAK_DEVICE": "",
|
||||
"drag_and_drop_hint": "",
|
||||
"AUTHENTICATE": "",
|
||||
"UPLOADED_TO_SINGLE_COLLECTION": "",
|
||||
"UPLOADED_TO_SEPARATE_COLLECTIONS": "",
|
||||
"NEVERMIND": "",
|
||||
"update_available": "",
|
||||
"update_installable_message": "",
|
||||
"install_now": "",
|
||||
"install_on_next_launch": "",
|
||||
"update_available_message": "",
|
||||
"download_and_install": "",
|
||||
"ignore_this_version": "",
|
||||
"TODAY": "",
|
||||
"YESTERDAY": "",
|
||||
"NAME_PLACEHOLDER": "",
|
||||
"ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "",
|
||||
"ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "",
|
||||
"CHOSE_THEME": "",
|
||||
"more_details": "",
|
||||
"ml_search": "",
|
||||
"ml_search_description": "",
|
||||
"ml_search_footnote": "",
|
||||
"indexing": "",
|
||||
"processed": "",
|
||||
"indexing_status_running": "",
|
||||
"indexing_status_fetching": "",
|
||||
"indexing_status_scheduled": "",
|
||||
"indexing_status_done": "",
|
||||
"ml_search_disable": "",
|
||||
"ml_search_disable_confirm": "",
|
||||
"ml_consent": "",
|
||||
"ml_consent_title": "",
|
||||
"ml_consent_description": "",
|
||||
"ml_consent_confirmation": "",
|
||||
"labs": "",
|
||||
"YOURS": "",
|
||||
"passphrase_strength_weak": "",
|
||||
"passphrase_strength_moderate": "",
|
||||
"passphrase_strength_strong": "",
|
||||
"preferences": "",
|
||||
"language": "",
|
||||
"advanced": "",
|
||||
"EXPORT_DIRECTORY_DOES_NOT_EXIST": "",
|
||||
"EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "",
|
||||
"SUBSCRIPTION_VERIFICATION_ERROR": "",
|
||||
"FAILED_UPLOADS": "Upload falhou ",
|
||||
"failed_uploads_hint": "Haverá uma opção para tentar novamente quando o upload terminar",
|
||||
"SKIPPED_FILES": "Uploads ignorados",
|
||||
"THUMBNAIL_GENERATION_FAILED_UPLOADS": "Falha ao gerar miniaturas",
|
||||
"UNSUPPORTED_FILES": "Arquivos não suportados",
|
||||
"SUCCESSFUL_UPLOADS": "Envios bem sucedidos",
|
||||
"SKIPPED_INFO": "Saltou estes ficheiros porque existem ficheiros com o mesmo nome e conteúdo no mesmo álbum",
|
||||
"UNSUPPORTED_INFO": "Ente ainda não suporta estes formatos de arquivo",
|
||||
"BLOCKED_UPLOADS": "Uploads bloqueados",
|
||||
"INPROGRESS_METADATA_EXTRACTION": "Em andamento",
|
||||
"INPROGRESS_UPLOADS": "Uploads em andamento",
|
||||
"TOO_LARGE_UPLOADS": "Arquivos grandes",
|
||||
"LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Armazenamento insuficiente",
|
||||
"LARGER_THAN_AVAILABLE_STORAGE_INFO": "Estes ficheiros não foram carregados porque excedem o limite máximo de tamanho do seu plano de armazenamento",
|
||||
"TOO_LARGE_INFO": "Estes ficheiros não foram carregados porque excedem o nosso limite máximo de tamanho de ficheiro",
|
||||
"THUMBNAIL_GENERATION_FAILED_INFO": "Estes ficheiros foram carregados, mas infelizmente não foi possível gerar as respectivas miniaturas.",
|
||||
"upload_to_album": "Carregar para o álbum",
|
||||
"add_to_album": "Adicionar ao álbum",
|
||||
"move_to_album": "Mover para álbum",
|
||||
"unhide_to_album": "Mostrar para o álbum",
|
||||
"restore_to_album": "Restaurar para álbum",
|
||||
"section_all": "Todos",
|
||||
"section_uncategorized": "Sem categoria",
|
||||
"section_archive": "Arquivado",
|
||||
"section_hidden": "Oculto",
|
||||
"section_trash": "Lixo",
|
||||
"favorites": "Favoritos",
|
||||
"archive": "Arquivar",
|
||||
"archive_album": "Arquivar álbum",
|
||||
"unarchive": "Desarquivar",
|
||||
"unarchive_album": "Desarquivar álbum",
|
||||
"hide_collection": "Ocultar álbum",
|
||||
"unhide_collection": "Mostrar álbum",
|
||||
"MOVE": "Mover",
|
||||
"add": "Adicionar",
|
||||
"REMOVE": "Remover",
|
||||
"YES_REMOVE": "Sim, remover",
|
||||
"REMOVE_FROM_COLLECTION": "Remover do álbum",
|
||||
"MOVE_TO_TRASH": "Mover para o lixo",
|
||||
"TRASH_FILES_MESSAGE": "Os ficheiros selecionados serão removidos de todos os álbuns e movidos para o lixo.",
|
||||
"TRASH_FILE_MESSAGE": "O ficheiro será removido de todos os álbuns e movido para o lixo.",
|
||||
"DELETE_PERMANENTLY": "Apagar permanentemente",
|
||||
"RESTORE": "Restaurar",
|
||||
"empty_trash": "Esvaziar lixo",
|
||||
"empty_trash_title": "Esvaziar lixo?",
|
||||
"empty_trash_message": "Estes ficheiros serão permanentemente eliminados da sua conta Ente.",
|
||||
"leave_album": "Sair do álbum",
|
||||
"leave_shared_album_title": "Sair do álbum compartilhado?",
|
||||
"leave_shared_album_message": "Sairá do álbum e este deixará de ser visível para si.",
|
||||
"leave_shared_album": "Sim, sair",
|
||||
"NOT_FILE_OWNER": "Não é possível apagar ficheiros de um álbum partilhado",
|
||||
"CONFIRM_SELF_REMOVE_MESSAGE": "Os itens selecionados serão removidos deste álbum. Os itens que estão apenas neste álbum serão movidos para Uncategorized.",
|
||||
"CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Alguns dos itens que está a remover foram adicionados por outras pessoas, pelo que perderá o acesso aos mesmos.",
|
||||
"sort_by_creation_time_ascending": "Mais antigo",
|
||||
"sort_by_updation_time_descending": "Última atualização",
|
||||
"sort_by_name": "Nome",
|
||||
"FIX_CREATION_TIME": "Corrigir hora",
|
||||
"FIX_CREATION_TIME_IN_PROGRESS": "Corrigindo horário",
|
||||
"CREATION_TIME_UPDATED": "Hora do arquivo atualizado",
|
||||
"UPDATE_CREATION_TIME_NOT_STARTED": "Selecione a opção que deseja usar",
|
||||
"UPDATE_CREATION_TIME_COMPLETED": "Todos os arquivos atualizados com sucesso",
|
||||
"UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "A atualização do horário falhou para alguns arquivos, por favor, tente novamente",
|
||||
"CAPTION_CHARACTER_LIMIT": "5000 caracteres no máximo",
|
||||
"DATE_TIME_ORIGINAL": "Exif: Data e Hora Original",
|
||||
"DATE_TIME_DIGITIZED": "Exif: Data e Hora Digitalizada",
|
||||
"METADATA_DATE": "Exif: Data de Metadados",
|
||||
"CUSTOM_TIME": "Tempo personalizado",
|
||||
"sharing_details": "Detalhes de compartilhamento",
|
||||
"modify_sharing": "Modificar compartilhamento",
|
||||
"ADD_COLLABORATORS": "Adicionar colaboradores",
|
||||
"ADD_NEW_EMAIL": "Adicionar um novo email",
|
||||
"shared_with_people_count_zero": "Partilhar com pessoas específicas",
|
||||
"shared_with_people_count_one": "Partilhado com 1 pessoa",
|
||||
"shared_with_people_count": "Partilhado com {{count, number}} pessoas",
|
||||
"participants_count_zero": "Nenhum participante",
|
||||
"participants_count_one": "1 participante",
|
||||
"participants_count": "{{count, number}} participantes",
|
||||
"ADD_VIEWERS": "Adicionar visualizações",
|
||||
"CHANGE_PERMISSIONS_TO_VIEWER": "<p>{{selectedEmail}} não poderá adicionar mais fotografias ao álbum</p><p>Ainda poderão remover fotografias adicionadas por eles</p>",
|
||||
"CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} poderá adicionar fotografias ao álbum",
|
||||
"CONVERT_TO_VIEWER": "Sim, converter para visualizador",
|
||||
"CONVERT_TO_COLLABORATOR": "Sim, converter para colaborador",
|
||||
"CHANGE_PERMISSION": "Alterar permissões?",
|
||||
"REMOVE_PARTICIPANT": "Remover?",
|
||||
"CONFIRM_REMOVE": "Sim, remover",
|
||||
"MANAGE": "Gerenciar",
|
||||
"ADDED_AS": "Adicionado como",
|
||||
"COLLABORATOR_RIGHTS": "Os colaboradores podem adicionar fotografias e vídeos ao álbum partilhado",
|
||||
"REMOVE_PARTICIPANT_HEAD": "Remover participante",
|
||||
"OWNER": "Proprietário",
|
||||
"COLLABORATORS": "Colaboradores",
|
||||
"ADD_MORE": "Adicionar mais",
|
||||
"VIEWERS": "Visualizadores",
|
||||
"OR_ADD_EXISTING": "Ou escolher um já existente",
|
||||
"REMOVE_PARTICIPANT_MESSAGE": "<p>{{selectedEmail}} será removido do álbum</p><p>Quaisquer fotografias adicionadas por ele também serão removidas do álbum</p>",
|
||||
"NOT_FOUND": "404 Página não encontrada",
|
||||
"LINK_EXPIRED": "Link expirado",
|
||||
"LINK_EXPIRED_MESSAGE": "Este link expirou ou foi desativado!",
|
||||
"MANAGE_LINK": "Gerir link",
|
||||
"LINK_TOO_MANY_REQUESTS": "Desculpe, este álbum foi visualizado em muitos dispositivos!",
|
||||
"FILE_DOWNLOAD": "Permitir downloads",
|
||||
"link_password_lock": "Bloqueio da palavra-passe",
|
||||
"PUBLIC_COLLECT": "Permitir adicionar fotos",
|
||||
"LINK_DEVICE_LIMIT": "Limite de dispositivos",
|
||||
"NO_DEVICE_LIMIT": "Nenhum",
|
||||
"LINK_EXPIRY": "Link expirado",
|
||||
"NEVER": "Nunca",
|
||||
"DISABLE_FILE_DOWNLOAD": "Desativar download",
|
||||
"DISABLE_FILE_DOWNLOAD_MESSAGE": "<p>Tem a certeza de que pretende desativar o botão de transferência de ficheiros? </p><p>Os espectadores podem ainda tirar capturas de ecrã ou guardar uma cópia das suas fotografias utilizando ferramentas externas.</p>",
|
||||
"SHARED_USING": "Partilhado utilizando ",
|
||||
"SHARING_REFERRAL_CODE": "Use o código <strong>{{referralCode}}</strong> para obter 10 GB de graça",
|
||||
"LIVE": "EM DIRETO",
|
||||
"DISABLE_PASSWORD": "Desativar o bloqueio da palavra-passe",
|
||||
"DISABLE_PASSWORD_MESSAGE": "Tem a certeza de que pretende desativar o bloqueio de palavra-passe?",
|
||||
"PASSWORD_LOCK": "Bloqueio da palavra-passe",
|
||||
"LOCK": "Bloquear",
|
||||
"file": "Arquivo",
|
||||
"folder": "Pasta",
|
||||
"google_takeout": "Google Takeout",
|
||||
"DEDUPLICATE_FILES": "Arquivos duplicados",
|
||||
"NO_DUPLICATES_FOUND": "Não existem ficheiros duplicados que possam ser eliminados",
|
||||
"FILES": "arquivos",
|
||||
"EACH": "cada",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "Os seguintes ficheiros foram agrupados com base nos seus tamanhos. Reveja e elimine os itens que considera duplicados",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "Tem a certeza de que pretende parar todos os carregamentos em curso?",
|
||||
"STOP_UPLOADS_HEADER": "Parar uploads?",
|
||||
"YES_STOP_UPLOADS": "Sim, parar uploads",
|
||||
"STOP_DOWNLOADS_HEADER": "Parar downloads?",
|
||||
"YES_STOP_DOWNLOADS": "Sim, parar downloads",
|
||||
"STOP_ALL_DOWNLOADS_MESSAGE": "Tem a certeza de que pretende parar todas as transferências em curso?",
|
||||
"albums": "Álbuns",
|
||||
"albums_count_one": "1 Álbum",
|
||||
"albums_count": "{{count, number}} Álbuns",
|
||||
"all_albums": "Todos os álbuns",
|
||||
"all_hidden_albums": "Todos os álbuns ocultos",
|
||||
"hidden_albums": "Álbuns ocultos",
|
||||
"hidden_items": "Itens ocultos",
|
||||
"ENTER_TWO_FACTOR_OTP": "Introduzir o código de 6 dígitos da\nsua aplicação de autenticação.",
|
||||
"create_account": "Criar conta",
|
||||
"COPIED": "Copiado",
|
||||
"WATCH_FOLDERS": "Pastas monitoradas",
|
||||
"upgrade_now": "Atualizar agora",
|
||||
"renew_now": "Renovar agora",
|
||||
"STORAGE": "Armazenamento",
|
||||
"USED": "utilizado",
|
||||
"YOU": "Tu",
|
||||
"FAMILY": "Família",
|
||||
"FREE": "grátis",
|
||||
"OF": "de",
|
||||
"WATCHED_FOLDERS": "Pastas monitoradas",
|
||||
"NO_FOLDERS_ADDED": "Nenhuma pasta adicionada ainda!",
|
||||
"FOLDERS_AUTOMATICALLY_MONITORED": "As pastas que adicionar aqui serão monitorizadas automaticamente",
|
||||
"UPLOAD_NEW_FILES_TO_ENTE": "Carregar novos ficheiros para o Ente",
|
||||
"REMOVE_DELETED_FILES_FROM_ENTE": "Remover ficheiros eliminados do Ente",
|
||||
"ADD_FOLDER": "Adicionar pasta",
|
||||
"STOP_WATCHING": "Parar de assistir",
|
||||
"STOP_WATCHING_FOLDER": "Deixar de ver a pasta?",
|
||||
"STOP_WATCHING_DIALOG_MESSAGE": "Os seus ficheiros existentes não serão eliminados, mas o Ente deixará de atualizar automaticamente o álbum Ente associado às alterações nesta pasta.",
|
||||
"YES_STOP": "Sim, parar",
|
||||
"CHANGE_FOLDER": "Alterar pasta",
|
||||
"FAMILY_PLAN": "Plano familiar",
|
||||
"debug_logs": "Logs de depuração",
|
||||
"download_logs": "Descarregar logs",
|
||||
"download_logs_message": "<p>Isto irá descarregar registos de depuração, que pode enviar-nos por correio eletrónico para ajudar a depurar o seu problema.</p><p>Por favor, note que os nomes dos ficheiros serão incluídos para ajudar a localizar problemas com ficheiros específicos.</p>",
|
||||
"WEAK_DEVICE": "O navegador Web que está a utilizar não é suficientemente potente para encriptar as suas fotografias. Tente iniciar sessão no Ente no seu computador ou descarregue a aplicação móvel/desktop do Ente.",
|
||||
"drag_and_drop_hint": "Ou arrastar e largar na janela Ente",
|
||||
"AUTHENTICATE": "Autenticar",
|
||||
"UPLOADED_TO_SINGLE_COLLECTION": "Carregado para uma coleção única",
|
||||
"UPLOADED_TO_SEPARATE_COLLECTIONS": "Carregado para colecções separadas",
|
||||
"NEVERMIND": "Esquecer",
|
||||
"update_available": "Atualização disponível",
|
||||
"update_installable_message": "Uma nova versão do Ente está pronta para ser instalada.",
|
||||
"install_now": "Instalar agora",
|
||||
"install_on_next_launch": "Instalar na próxima inicialização",
|
||||
"update_available_message": "Foi lançada uma nova versão do Ente, mas não pode ser descarregada e instalada automaticamente.",
|
||||
"download_and_install": "Descarregar e instalar",
|
||||
"ignore_this_version": "Ignorar esta versão",
|
||||
"TODAY": "Hoje",
|
||||
"YESTERDAY": "Ontem",
|
||||
"NAME_PLACEHOLDER": "Nome...",
|
||||
"ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "Não foi possível criar álbuns a partir da mistura de arquivos/pastas",
|
||||
"ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "<p>Arrastou e largou uma mistura de ficheiros e pastas.</p><p>Por favor, forneça apenas ficheiros ou apenas pastas quando selecionar a opção para criar álbuns separados</p>",
|
||||
"CHOSE_THEME": "Escolher tema",
|
||||
"more_details": "Mais detalhes",
|
||||
"ml_search": "Aprendizagem automática",
|
||||
"ml_search_description": "O Ente suporta a aprendizagem automática no dispositivo para reconhecimento facial, pesquisa mágica e outras funcionalidades de pesquisa avançadas",
|
||||
"ml_search_footnote": "A pesquisa mágica permite pesquisar fotografias pelo seu conteúdo, por exemplo, “carro”, “carro vermelho”, “Ferrari",
|
||||
"indexing": "Indexar",
|
||||
"processed": "Processado",
|
||||
"indexing_status_running": "Em execução",
|
||||
"indexing_status_fetching": "A procurar",
|
||||
"indexing_status_scheduled": "Agendado",
|
||||
"indexing_status_done": "Concluído",
|
||||
"ml_search_disable": "Desativar aprendizado automático",
|
||||
"ml_search_disable_confirm": "Pretende desativar a aprendizagem automática em todos os seus dispositivos?",
|
||||
"ml_consent": "Ativar aprendizagem automática",
|
||||
"ml_consent_title": "Ativar aprendizagem automática?",
|
||||
"ml_consent_description": "<p>Se ativar a aprendizagem automática, o Ente extrairá informações como a geometria do rosto de ficheiros, incluindo os partilhados consigo.</p><p>Isto acontecerá no seu dispositivo e qualquer informação biométrica gerada será encriptada de ponta a ponta.</p><p><a>Clique aqui para obter mais detalhes sobre esta funcionalidade na nossa política de privacidade</a></p>",
|
||||
"ml_consent_confirmation": "Eu entendo, e desejo ativar a aprendizagem automática",
|
||||
"labs": "Laboratórios",
|
||||
"YOURS": "seu",
|
||||
"passphrase_strength_weak": "Força da palavra-passe: Fraca",
|
||||
"passphrase_strength_moderate": "Força da palavra-passe: Moderada",
|
||||
"passphrase_strength_strong": "Força da palavra-passe: Forte",
|
||||
"preferences": "Preferências",
|
||||
"language": "Idioma",
|
||||
"advanced": "Avançado",
|
||||
"EXPORT_DIRECTORY_DOES_NOT_EXIST": "Diretório de exportação inválido",
|
||||
"EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "<p>O diretório de exportação que selecionou não existe.</p><p>Por favor, selecione um diretório válido.</p>",
|
||||
"SUBSCRIPTION_VERIFICATION_ERROR": "Falha na verificação da subscrição",
|
||||
"storage_unit": {
|
||||
"b": "",
|
||||
"kb": "",
|
||||
"mb": "",
|
||||
"gb": "",
|
||||
"tb": ""
|
||||
"b": "B",
|
||||
"kb": "KB",
|
||||
"mb": "MB",
|
||||
"gb": "GB",
|
||||
"tb": "TB"
|
||||
},
|
||||
"AFTER_TIME": {
|
||||
"HOUR": "",
|
||||
"DAY": "",
|
||||
"WEEK": "",
|
||||
"MONTH": "",
|
||||
"YEAR": ""
|
||||
"HOUR": "após uma hora",
|
||||
"DAY": "após um dia",
|
||||
"WEEK": "após uma semana",
|
||||
"MONTH": "após um mês",
|
||||
"YEAR": "após um ano"
|
||||
},
|
||||
"COPY_LINK": "",
|
||||
"DONE": "",
|
||||
"LINK_SHARE_TITLE": "",
|
||||
"REMOVE_LINK": "",
|
||||
"CREATE_PUBLIC_SHARING": "",
|
||||
"PUBLIC_LINK_CREATED": "",
|
||||
"PUBLIC_LINK_ENABLED": "",
|
||||
"COLLECT_PHOTOS": "",
|
||||
"PUBLIC_COLLECT_SUBTEXT": "",
|
||||
"STOP_EXPORT": "",
|
||||
"EXPORT_PROGRESS": "",
|
||||
"MIGRATING_EXPORT": "",
|
||||
"RENAMING_COLLECTION_FOLDERS": "",
|
||||
"TRASHING_DELETED_FILES": "",
|
||||
"TRASHING_DELETED_COLLECTIONS": "",
|
||||
"CONTINUOUS_EXPORT": "",
|
||||
"PENDING_ITEMS": "",
|
||||
"EXPORT_STARTING": "",
|
||||
"delete_account_reason_label": "",
|
||||
"delete_account_reason_placeholder": "",
|
||||
"COPY_LINK": "Copiar link",
|
||||
"DONE": "Concluído",
|
||||
"LINK_SHARE_TITLE": "Ou partilhar uma link",
|
||||
"REMOVE_LINK": "Remover link",
|
||||
"CREATE_PUBLIC_SHARING": "Criar link público",
|
||||
"PUBLIC_LINK_CREATED": "Link público criado",
|
||||
"PUBLIC_LINK_ENABLED": "Link público ativado",
|
||||
"COLLECT_PHOTOS": "Recolher fotos",
|
||||
"PUBLIC_COLLECT_SUBTEXT": "Permitir que as pessoas com a ligação também adicionem fotos ao álbum partilhado.",
|
||||
"STOP_EXPORT": "Parar",
|
||||
"EXPORT_PROGRESS": "<a>{{progress.success, number}} / {{progress.total, number}}</a> itens sincronizados",
|
||||
"MIGRATING_EXPORT": "Preparar...",
|
||||
"RENAMING_COLLECTION_FOLDERS": "Renomear pastas de álbuns...",
|
||||
"TRASHING_DELETED_FILES": "Eliminar arquivos apagados...",
|
||||
"TRASHING_DELETED_COLLECTIONS": "Eliminar álbuns apagados...",
|
||||
"CONTINUOUS_EXPORT": "Sincronização contínua",
|
||||
"PENDING_ITEMS": "Itens pendentes",
|
||||
"EXPORT_STARTING": "Iniciar a exportação...",
|
||||
"delete_account_reason_label": "Qual o principal motivo pelo qual está a eliminar a conta?",
|
||||
"delete_account_reason_placeholder": "Selecione um motivo",
|
||||
"delete_reason": {
|
||||
"missing_feature": "",
|
||||
"behaviour": "",
|
||||
"found_another_service": "",
|
||||
"not_listed": ""
|
||||
"missing_feature": "Falta uma chave que eu preciso",
|
||||
"behaviour": "O aplicativo ou um determinado recurso não se comportou como era suposto",
|
||||
"found_another_service": "Encontrei outro serviço que gosto mais",
|
||||
"not_listed": "O motivo não está na lista"
|
||||
},
|
||||
"delete_account_feedback_label": "",
|
||||
"delete_account_feedback_placeholder": "",
|
||||
"delete_account_confirm_checkbox_label": "",
|
||||
"delete_account_confirm": "",
|
||||
"delete_account_confirm_message": "",
|
||||
"feedback_required": "",
|
||||
"feedback_required_found_another_service": "",
|
||||
"RECOVER_TWO_FACTOR": "",
|
||||
"at": "",
|
||||
"AUTH_NEXT": "",
|
||||
"AUTH_DOWNLOAD_MOBILE_APP": "",
|
||||
"HIDE": "",
|
||||
"UNHIDE": "",
|
||||
"sort_by": "",
|
||||
"newest_first": "",
|
||||
"oldest_first": "",
|
||||
"CONVERSION_FAILED_NOTIFICATION_MESSAGE": "",
|
||||
"pin_album": "",
|
||||
"unpin_album": "",
|
||||
"DOWNLOAD_COMPLETE": "",
|
||||
"DOWNLOADING_COLLECTION": "",
|
||||
"DOWNLOAD_FAILED": "",
|
||||
"DOWNLOAD_PROGRESS": "",
|
||||
"CHRISTMAS": "",
|
||||
"CHRISTMAS_EVE": "",
|
||||
"NEW_YEAR": "",
|
||||
"NEW_YEAR_EVE": "",
|
||||
"IMAGE": "",
|
||||
"VIDEO": "",
|
||||
"LIVE_PHOTO": "",
|
||||
"delete_account_feedback_label": "Lamentamos a sua partida. Indique-nos a razão para podermos melhorar o serviço.",
|
||||
"delete_account_feedback_placeholder": "Feedback",
|
||||
"delete_account_confirm_checkbox_label": "Sim, quero apagar permanentemente esta conta e todos os seus dados",
|
||||
"delete_account_confirm": "Confirmar eliminação da conta",
|
||||
"delete_account_confirm_message": "<p>Esta conta está ligada a outras aplicações Ente, se utilizar alguma.</p><p>Os seus dados carregados, em todas as aplicações Ente, serão agendados para eliminação e a sua conta será permanentemente eliminada.</p>",
|
||||
"feedback_required": "Por favor, ajude-nos com esta informação",
|
||||
"feedback_required_found_another_service": "O que o outro serviço faz melhor?",
|
||||
"RECOVER_TWO_FACTOR": "Recuperar dois fatores",
|
||||
"at": "em",
|
||||
"AUTH_NEXT": "seguinte",
|
||||
"AUTH_DOWNLOAD_MOBILE_APP": "Descarregue a nossa aplicação móvel para gerir os seus segredos",
|
||||
"HIDE": "Ocultar",
|
||||
"UNHIDE": "Mostrar",
|
||||
"sort_by": "Ordenar por",
|
||||
"newest_first": "Mais recentes primeiro",
|
||||
"oldest_first": "Mais antigo primeiro",
|
||||
"CONVERSION_FAILED_NOTIFICATION_MESSAGE": "Este arquivo não pôde ser visualizado. Clique aqui para fazer o download original.",
|
||||
"pin_album": "Fixar álbum",
|
||||
"unpin_album": "Desafixar álbum",
|
||||
"DOWNLOAD_COMPLETE": "Download concluído",
|
||||
"DOWNLOADING_COLLECTION": "Fazer download de {{name}}",
|
||||
"DOWNLOAD_FAILED": "Falha no download",
|
||||
"DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} arquivos",
|
||||
"CHRISTMAS": "Natal",
|
||||
"CHRISTMAS_EVE": "Véspera de Natal",
|
||||
"NEW_YEAR": "Ano Novo",
|
||||
"NEW_YEAR_EVE": "Véspera de Ano Novo",
|
||||
"IMAGE": "Imagem",
|
||||
"VIDEO": "Vídeo",
|
||||
"LIVE_PHOTO": "Fotos em movimento",
|
||||
"editor": {
|
||||
"crop": ""
|
||||
"crop": "Recortar"
|
||||
},
|
||||
"CONVERT": "",
|
||||
"confirm_editor_close": "",
|
||||
"confirm_editor_close_message": "",
|
||||
"BRIGHTNESS": "",
|
||||
"CONTRAST": "",
|
||||
"SATURATION": "",
|
||||
"BLUR": "",
|
||||
"INVERT_COLORS": "",
|
||||
"ASPECT_RATIO": "",
|
||||
"SQUARE": "",
|
||||
"ROTATE_LEFT": "",
|
||||
"ROTATE_RIGHT": "",
|
||||
"FLIP_VERTICALLY": "",
|
||||
"FLIP_HORIZONTALLY": "",
|
||||
"DOWNLOAD_EDITED": "",
|
||||
"SAVE_A_COPY_TO_ENTE": "",
|
||||
"RESTORE_ORIGINAL": "",
|
||||
"TRANSFORM": "",
|
||||
"COLORS": "",
|
||||
"FLIP": "",
|
||||
"ROTATION": "",
|
||||
"reset": "",
|
||||
"PHOTO_EDITOR": "",
|
||||
"CONVERT": "Converter",
|
||||
"confirm_editor_close": "Tem certeza de que deseja fechar o editor?",
|
||||
"confirm_editor_close_message": "Descarregue a imagem editada ou guarde uma cópia no Ente para manter as alterações.",
|
||||
"BRIGHTNESS": "Brilho",
|
||||
"CONTRAST": "Contraste",
|
||||
"SATURATION": "Saturação",
|
||||
"BLUR": "Desfoque",
|
||||
"INVERT_COLORS": "Inverter Cores",
|
||||
"ASPECT_RATIO": "Proporção da imagem",
|
||||
"SQUARE": "Quadrado",
|
||||
"ROTATE_LEFT": "Rodar para a esquerda",
|
||||
"ROTATE_RIGHT": "Rodar para a direita",
|
||||
"FLIP_VERTICALLY": "Inverter verticalmente",
|
||||
"FLIP_HORIZONTALLY": "Inverter horizontalmente",
|
||||
"DOWNLOAD_EDITED": "Descarregar Editado",
|
||||
"SAVE_A_COPY_TO_ENTE": "Salvar uma cópia para o Ente",
|
||||
"RESTORE_ORIGINAL": "Restaurar original",
|
||||
"TRANSFORM": "Transformar",
|
||||
"COLORS": "Cores",
|
||||
"FLIP": "Inverter",
|
||||
"ROTATION": "Rotação",
|
||||
"reset": "Restaurar",
|
||||
"PHOTO_EDITOR": "Editor de Fotos",
|
||||
"FASTER_UPLOAD": "",
|
||||
"FASTER_UPLOAD_DESCRIPTION": "",
|
||||
"cast_album_to_tv": "",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { MenuItemGroup } from "@/base/components/Menu";
|
||||
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import type { NestedDrawerVisibilityProps } from "@/base/components/utils/modal";
|
||||
import { disableML, enableML, type MLStatus } from "@/new/photos/services/ml";
|
||||
@@ -67,7 +67,7 @@ export const MLSettings: React.FC<NestedDrawerVisibilityProps> = ({
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<EnteDrawer
|
||||
<SidebarDrawer
|
||||
anchor="left"
|
||||
transitionDuration={0}
|
||||
open={open}
|
||||
@@ -84,7 +84,7 @@ export const MLSettings: React.FC<NestedDrawerVisibilityProps> = ({
|
||||
/>
|
||||
{component}
|
||||
</Stack>
|
||||
</EnteDrawer>
|
||||
</SidebarDrawer>
|
||||
|
||||
<FaceConsent
|
||||
open={openFaceConsent}
|
||||
@@ -174,7 +174,7 @@ const FaceConsent: React.FC<FaceConsentProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<EnteDrawer
|
||||
<SidebarDrawer
|
||||
transitionDuration={0}
|
||||
open={open}
|
||||
onClose={handleDrawerClose}
|
||||
@@ -233,7 +233,7 @@ const FaceConsent: React.FC<FaceConsentProps> = ({
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</EnteDrawer>
|
||||
</SidebarDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
26
web/packages/new/photos/components/utils/use-loading-bar.ts
Normal file
26
web/packages/new/photos/components/utils/use-loading-bar.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
import { type LoadingBarRef } from "react-top-loading-bar";
|
||||
|
||||
/**
|
||||
* A convenience hook for returning stable functions tied to a
|
||||
* {@link LoadingBar} ref.
|
||||
*
|
||||
* The {@link LoadingBar} component comes from the "react-top-loading-bar"
|
||||
* library. To control it, we keep a ref. We want to allow components in our
|
||||
* React tree to be able to also control the loading bar, but instead of
|
||||
* exposing the ref directly, we export wrapper functions to start and stop the
|
||||
* loading bar. This hook returns these functions (and the ref).
|
||||
*/
|
||||
export const useLoadingBar = () => {
|
||||
const loadingBarRef = useRef<LoadingBarRef>();
|
||||
|
||||
const showLoadingBar = useCallback(() => {
|
||||
loadingBarRef.current?.continuousStart();
|
||||
}, []);
|
||||
|
||||
const hideLoadingBar = useCallback(() => {
|
||||
loadingBarRef.current?.complete();
|
||||
}, []);
|
||||
|
||||
return { loadingBarRef, showLoadingBar, hideLoadingBar };
|
||||
};
|
||||
@@ -15,18 +15,18 @@ import { useAppContext } from "../../types/context";
|
||||
export const useWrapAsyncOperation = <T extends unknown[]>(
|
||||
f: (...args: T) => Promise<void>,
|
||||
) => {
|
||||
const { startLoading, finishLoading, onGenericError } = useAppContext();
|
||||
const { showLoadingBar, hideLoadingBar, onGenericError } = useAppContext();
|
||||
return useCallback(
|
||||
async (...args: T) => {
|
||||
startLoading();
|
||||
showLoadingBar();
|
||||
try {
|
||||
await f(...args);
|
||||
} catch (e) {
|
||||
onGenericError(e);
|
||||
} finally {
|
||||
finishLoading();
|
||||
hideLoadingBar();
|
||||
}
|
||||
},
|
||||
[f, startLoading, finishLoading, onGenericError],
|
||||
[f, showLoadingBar, hideLoadingBar, onGenericError],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import { lowercaseExtension } from "@/base/file";
|
||||
import log from "@/base/log";
|
||||
import type { EnteFile } from "@/media/file";
|
||||
import { FileType } from "@/media/file-type";
|
||||
import piexif from "piexifjs";
|
||||
|
||||
/**
|
||||
* Return a new stream after applying Exif updates if applicable to the given
|
||||
* stream, otherwise return the original.
|
||||
*
|
||||
* This function is meant to provide a stream that can be used to download (or
|
||||
* export) a file to the user's computer after applying any Exif updates to the
|
||||
* original file's data.
|
||||
*
|
||||
* - This only updates JPEG files.
|
||||
*
|
||||
* - For JPEG files, the DateTimeOriginal Exif entry is updated to reflect the
|
||||
* time that the user edited within Ente.
|
||||
*
|
||||
* @param file The {@link EnteFile} whose data we want.
|
||||
*
|
||||
* @param stream A {@link ReadableStream} containing the original data for
|
||||
* {@link file}.
|
||||
*
|
||||
* @returns A new {@link ReadableStream} with updates if any updates were
|
||||
* needed, otherwise return the original stream.
|
||||
*/
|
||||
export const updateExifIfNeededAndPossible = async (
|
||||
file: EnteFile,
|
||||
stream: ReadableStream<Uint8Array>,
|
||||
): Promise<ReadableStream<Uint8Array>> => {
|
||||
// Not needed: Not an image.
|
||||
if (file.metadata.fileType != FileType.image) return stream;
|
||||
|
||||
// Not needed: Time was not edited.
|
||||
if (!file.pubMagicMetadata?.data.editedTime) return stream;
|
||||
|
||||
const fileName = file.metadata.title;
|
||||
const extension = lowercaseExtension(fileName);
|
||||
// Not possible: Not a JPEG (likely).
|
||||
if (extension != "jpeg" && extension != "jpg") return stream;
|
||||
|
||||
const blob = await new Response(stream).blob();
|
||||
try {
|
||||
const updatedBlob = await setJPEGExifDateTimeOriginal(
|
||||
blob,
|
||||
new Date(file.pubMagicMetadata.data.editedTime / 1000),
|
||||
);
|
||||
return updatedBlob.stream();
|
||||
} catch (e) {
|
||||
log.error(`Failed to modify Exif date for ${fileName}`, e);
|
||||
// Ignore errors and use the original - we don't want to block the whole
|
||||
// download or export for an errant file. TODO: This is not always going
|
||||
// to be the correct choice, but instead trying further hack around with
|
||||
// the Exif modifications (and all the caveats that come with it), a
|
||||
// more principled approach is to put our metadata in a sidecar and
|
||||
// never touch the original. We can then and provide additional tools to
|
||||
// update the original if the user so wishes from the sidecar.
|
||||
return blob.stream();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a new blob with the "DateTimeOriginal" Exif tag set to the given
|
||||
* {@link date}.
|
||||
*
|
||||
* @param jpegBlob A {@link Blob} containing JPEG data.
|
||||
*
|
||||
* @param date A {@link Date} to use as the value for the Exif
|
||||
* "DateTimeOriginal" tag.
|
||||
*
|
||||
* @returns A new blob derived from {@link jpegBlob} but with the updated date.
|
||||
*/
|
||||
const setJPEGExifDateTimeOriginal = async (jpegBlob: Blob, date: Date) => {
|
||||
let dataURL = await blobToDataURL(jpegBlob);
|
||||
// Since we pass a Blob without an associated type, we get back a generic
|
||||
// data URL of the form "data:application/octet-stream;base64,...".
|
||||
//
|
||||
// Modify it to have a `image/jpeg` MIME type.
|
||||
dataURL = "data:image/jpeg;base64" + dataURL.slice(dataURL.indexOf(","));
|
||||
|
||||
const exifObj = piexif.load(dataURL);
|
||||
if (!exifObj.Exif) exifObj.Exif = {};
|
||||
exifObj.Exif[piexif.ExifIFD.DateTimeOriginal] =
|
||||
convertToExifDateFormat(date);
|
||||
const exifBytes = piexif.dump(exifObj);
|
||||
const exifInsertedFile = piexif.insert(exifBytes, dataURL);
|
||||
|
||||
return dataURLToBlob(exifInsertedFile);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a blob to a `data:` URL.
|
||||
*/
|
||||
const blobToDataURL = (blob: Blob) =>
|
||||
new Promise<string>((resolve) => {
|
||||
const reader = new FileReader();
|
||||
// We need to cast to a string here. This should be safe since MDN says:
|
||||
//
|
||||
// > the result attribute contains the data as a data: URL representing
|
||||
// > the file's data as a base64 encoded string.
|
||||
// >
|
||||
// > https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
/**
|
||||
* Convert a `data:` URL to a blob.
|
||||
*
|
||||
* Requires `connect-src data:` in the CSP (since it internally uses `fetch` to
|
||||
* perform the conversion).
|
||||
*/
|
||||
const dataURLToBlob = (dataURI: string) =>
|
||||
fetch(dataURI).then((res) => res.blob());
|
||||
|
||||
/**
|
||||
* Convert the given {@link Date} to a format that is expected by Exif for the
|
||||
* DateTimeOriginal tag.
|
||||
*
|
||||
* See: [Note: Exif dates]
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* TODO: This functionality is deprecated. The library we use here is
|
||||
* unmaintained and there are no comprehensive other JS libs.
|
||||
*
|
||||
* Instead of doing this in this selective way, we should provide a CLI tool
|
||||
* with better format support and more comprehensive handling of Exif and other
|
||||
* metadata fields (like captions) that can be used by the user to modify their
|
||||
* original from the Ente sidecar if they so wish.
|
||||
*/
|
||||
const convertToExifDateFormat = (date: Date) => {
|
||||
const YYYY = zeroPad(date.getFullYear(), 4);
|
||||
// JavaScript getMonth is zero-indexed, we want one-indexed.
|
||||
const MM = zeroPad(date.getMonth() + 1, 2);
|
||||
// JavaScript getDate is NOT zero-indexed, it is already one-indexed.
|
||||
const DD = zeroPad(date.getDate(), 2);
|
||||
const HH = zeroPad(date.getHours(), 2);
|
||||
const mm = zeroPad(date.getMinutes(), 2);
|
||||
const ss = zeroPad(date.getSeconds(), 2);
|
||||
|
||||
return `${YYYY}:${MM}:${DD} ${HH}:${mm}:${ss}`;
|
||||
};
|
||||
|
||||
/** Zero pad the given number to {@link d} digits. */
|
||||
const zeroPad = (n: number, d: number) => n.toString().padStart(d, "0");
|
||||
@@ -1,128 +0,0 @@
|
||||
import { authenticatedRequestHeaders, ensureOk } from "@/base/http";
|
||||
import { localUser } from "@/base/local-user";
|
||||
import log from "@/base/log";
|
||||
import { apiURL } from "@/base/origins";
|
||||
import { nullToUndefined } from "@/utils/transform";
|
||||
import { z } from "zod";
|
||||
|
||||
let _fetchTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
let _haveFetched = false;
|
||||
|
||||
/**
|
||||
* Fetch feature flags (potentially user specific) from remote and save them in
|
||||
* local storage for subsequent lookup.
|
||||
*
|
||||
* It fetches only once per session, and so is safe to call as arbitrarily many
|
||||
* times. Remember to call {@link clearFeatureFlagSessionState} on logout to
|
||||
* clear any in memory state so that these can be fetched again on the
|
||||
* subsequent login.
|
||||
*
|
||||
* [Note: Feature Flags]
|
||||
*
|
||||
* The workflow with feature flags is:
|
||||
*
|
||||
* 1. On app start feature flags are fetched once and saved in local storage. If
|
||||
* this fetch fails, we try again periodically (on every "sync") until
|
||||
* success.
|
||||
*
|
||||
* 2. Attempts to access any individual feature flage (e.g.
|
||||
* {@link isInternalUser}) returns the corresponding value from local storage
|
||||
* (substituting a default if needed).
|
||||
*
|
||||
* 3. However, if perchance the fetch-on-app-start hasn't completed yet (or had
|
||||
* failed), then a new fetch is tried. If even this fetch fails, we return
|
||||
* the default. Otherwise the now fetched result is saved to local storage
|
||||
* and the corresponding value returned.
|
||||
*/
|
||||
export const triggerFeatureFlagsFetchIfNeeded = () => {
|
||||
if (_haveFetched) return;
|
||||
if (_fetchTimeout) return;
|
||||
// Not critical, so fetch these after some delay.
|
||||
_fetchTimeout = setTimeout(() => {
|
||||
_fetchTimeout = undefined;
|
||||
void fetchAndSaveFeatureFlags().then(() => {
|
||||
_haveFetched = true;
|
||||
});
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
export const clearFeatureFlagSessionState = () => {
|
||||
if (_fetchTimeout) {
|
||||
clearTimeout(_fetchTimeout);
|
||||
_fetchTimeout = undefined;
|
||||
}
|
||||
_haveFetched = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch feature flags (potentially user specific) from remote and save them in
|
||||
* local storage for subsequent lookup.
|
||||
*/
|
||||
const fetchAndSaveFeatureFlags = () =>
|
||||
fetchFeatureFlags()
|
||||
.then((res) => res.text())
|
||||
.then(saveFlagJSONString);
|
||||
|
||||
const fetchFeatureFlags = async () => {
|
||||
const res = await fetch(await apiURL("/remote-store/feature-flags"), {
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
});
|
||||
ensureOk(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
const saveFlagJSONString = (s: string) =>
|
||||
localStorage.setItem("remoteFeatureFlags", s);
|
||||
|
||||
const remoteFeatureFlags = () => {
|
||||
const s = localStorage.getItem("remoteFeatureFlags");
|
||||
if (!s) return undefined;
|
||||
return FeatureFlags.parse(JSON.parse(s));
|
||||
};
|
||||
|
||||
const FeatureFlags = z.object({
|
||||
internalUser: z.boolean().nullish().transform(nullToUndefined),
|
||||
betaUser: z.boolean().nullish().transform(nullToUndefined),
|
||||
});
|
||||
|
||||
type FeatureFlags = z.infer<typeof FeatureFlags>;
|
||||
|
||||
const remoteFeatureFlagsFetchingIfNeeded = async () => {
|
||||
let ff = remoteFeatureFlags();
|
||||
if (!ff) {
|
||||
try {
|
||||
await fetchAndSaveFeatureFlags();
|
||||
} catch (e) {
|
||||
log.warn("Ignoring error when fetching feature flags", e);
|
||||
}
|
||||
ff = remoteFeatureFlags();
|
||||
}
|
||||
return ff;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return `true` if the current user is marked as an "internal" user.
|
||||
*
|
||||
* 1. Emails that end in `@ente.io` are considered as internal users.
|
||||
* 2. If the "internalUser" remote feature flag is set, the user is internal.
|
||||
* 3. Otherwise false.
|
||||
*
|
||||
* See also: [Note: Feature Flags].
|
||||
*/
|
||||
export const isInternalUser = async () => {
|
||||
const user = localUser();
|
||||
if (user?.email.endsWith("@ente.io")) return true;
|
||||
|
||||
const flags = await remoteFeatureFlagsFetchingIfNeeded();
|
||||
return flags?.internalUser ?? false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return `true` if the current user is marked as a "beta" user.
|
||||
*
|
||||
* See also: [Note: Feature Flags].
|
||||
*/
|
||||
export const isBetaUser = async () => {
|
||||
const flags = await remoteFeatureFlagsFetchingIfNeeded();
|
||||
return flags?.betaUser ?? false;
|
||||
};
|
||||
@@ -41,9 +41,9 @@ import type { CLIPMatches } from "./worker-types";
|
||||
/**
|
||||
* Internal state of the ML subsystem.
|
||||
*
|
||||
* This are essentially cached values used by the functions of this module.
|
||||
* These are essentially cached values used by the functions of this module.
|
||||
*
|
||||
* This should be cleared on logout.
|
||||
* They will be cleared on logout.
|
||||
*/
|
||||
class MLState {
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,38 @@ import { authenticatedRequestHeaders, ensureOk } from "@/base/http";
|
||||
import { apiURL } from "@/base/origins";
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* [Note: Remote store]
|
||||
*
|
||||
* The remote store provides a unified interface for persisting varied "remote
|
||||
* flags":
|
||||
*
|
||||
* - User preferences like "mapEnabled"
|
||||
*
|
||||
* - Feature flags like "isInternalUser"
|
||||
*
|
||||
* There are two APIs to get the current state from remote:
|
||||
*
|
||||
* 1. GET /remote-store/feature-flags fetches the combined state (nb: even
|
||||
* though the name of the endpoint has the word feature-flags, it also
|
||||
* includes user preferences).
|
||||
*
|
||||
* 2. GET /remote-store fetches individual values.
|
||||
*
|
||||
* Usually 1 is what we use, since it gets us everything in a single go, and
|
||||
* which we can also easily cache in local storage by saving the entire response
|
||||
* JSON blob.
|
||||
*
|
||||
* There is a single API (/remote-store/update) to update the state on remote.
|
||||
*/
|
||||
export const fetchFeatureFlags = async () => {
|
||||
const res = await fetch(await apiURL("/remote-store/feature-flags"), {
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
});
|
||||
ensureOk(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch the value for the given {@link key} from remote store.
|
||||
*
|
||||
|
||||
153
web/packages/new/photos/services/settings.ts
Normal file
153
web/packages/new/photos/services/settings.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* @file Storage (in-memory, local, remote) and update of various settings.
|
||||
*/
|
||||
|
||||
import { localUser } from "@/base/local-user";
|
||||
import log from "@/base/log";
|
||||
import { nullToUndefined } from "@/utils/transform";
|
||||
import { z } from "zod";
|
||||
import { fetchFeatureFlags } from "./remote-store";
|
||||
|
||||
/**
|
||||
* Internal in-memory state shared by the functions in this module.
|
||||
*
|
||||
* This entire object will be reset on logout.
|
||||
*/
|
||||
class SettingsState {
|
||||
/**
|
||||
* An arbitrary token to identify the current login.
|
||||
*
|
||||
* It is used to discard stale completions.
|
||||
*/
|
||||
id: number;
|
||||
|
||||
constructor() {
|
||||
this.id = Math.random();
|
||||
}
|
||||
|
||||
/**
|
||||
* True if we have performed a fetch for the logged in user since the app
|
||||
* started.
|
||||
*/
|
||||
haveSynced = false;
|
||||
|
||||
/**
|
||||
* In-memory flag that tracks if the current user is an internal user.
|
||||
*
|
||||
* See: [Note: Remote flag lifecycle].
|
||||
*/
|
||||
isInternalUser = false;
|
||||
|
||||
/**
|
||||
* In-memory flag that tracks if maps are enabled.
|
||||
*
|
||||
* See: [Note: Remote flag lifecycle].
|
||||
*/
|
||||
isMapEnabled = false;
|
||||
}
|
||||
|
||||
/** State shared by the functions in this module. See {@link SettingsState}. */
|
||||
let _state = new SettingsState();
|
||||
|
||||
/**
|
||||
* Fetch remote flags (feature flags and other user specific preferences) from
|
||||
* remote and save them in local storage for subsequent lookup.
|
||||
*
|
||||
* It fetches only once per app lifetime, and so is safe to call as arbitrarily
|
||||
* many times. Remember to call {@link clearFeatureFlagSessionState} on logout
|
||||
* to clear any in memory state so that these can be fetched again on the
|
||||
* subsequent login.
|
||||
*
|
||||
* The local cache will also be updated if an individual flag is changed.
|
||||
*
|
||||
* [Note: Remote flag lifecycle]
|
||||
*
|
||||
* At a high level, this is how the app manages remote flags:
|
||||
*
|
||||
* 1. On app start, the initial are read from local storage in
|
||||
* {@link initSettings}.
|
||||
*
|
||||
* 2. On app start, as part of the normal sync with remote, remote flags are
|
||||
* fetched once and saved in local storage, and the in-memory state updated
|
||||
* to reflect the latest values ({@link triggerSettingsSyncIfNeeded}). If
|
||||
* this fetch fails, we try again periodically (on every sync with remote)
|
||||
* until success.
|
||||
*
|
||||
* 3. Some operations like opening the preferences panel or updating a value
|
||||
* also cause an unconditional fetch and update ({@link syncSettings}).
|
||||
*
|
||||
* 4. The individual getter functions for the flags (e.g.
|
||||
* {@link isInternalUser}) return the in-memory values, and so are suitable
|
||||
* for frequent use during UI rendering.
|
||||
*
|
||||
* 5. Everything gets reset to the default state on {@link logoutSettings}.
|
||||
*/
|
||||
export const triggerSettingsSyncIfNeeded = () => {
|
||||
if (!_state.haveSynced) void syncSettings();
|
||||
};
|
||||
|
||||
/**
|
||||
* Read in the locally persisted settings into memory, but otherwise do not
|
||||
* initiate any network requests to fetch the latest values.
|
||||
*
|
||||
* This assumes that the user is already logged in.
|
||||
*/
|
||||
export const initSettings = () => {
|
||||
readInMemoryFlagsFromLocalStorage();
|
||||
};
|
||||
|
||||
export const logoutSettings = () => {
|
||||
_state = new SettingsState();
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch remote flags from remote and save them in local storage for subsequent
|
||||
* lookup. Then use the results to update our in memory state if needed.
|
||||
*/
|
||||
export const syncSettings = async () => {
|
||||
const id = _state.id;
|
||||
const jsonString = await fetchFeatureFlags().then((res) => res.text());
|
||||
if (_state.id != id) {
|
||||
log.info("Discarding stale settings sync not for the current login");
|
||||
return;
|
||||
}
|
||||
saveRemoteFeatureFlagsJSONString(jsonString);
|
||||
readInMemoryFlagsFromLocalStorage();
|
||||
_state.haveSynced = true;
|
||||
};
|
||||
|
||||
const saveRemoteFeatureFlagsJSONString = (s: string) =>
|
||||
localStorage.setItem("remoteFeatureFlags", s);
|
||||
|
||||
const savedRemoteFeatureFlags = () => {
|
||||
const s = localStorage.getItem("remoteFeatureFlags");
|
||||
if (!s) return undefined;
|
||||
return FeatureFlags.parse(JSON.parse(s));
|
||||
};
|
||||
|
||||
const FeatureFlags = z.object({
|
||||
internalUser: z.boolean().nullish().transform(nullToUndefined),
|
||||
betaUser: z.boolean().nullish().transform(nullToUndefined),
|
||||
});
|
||||
|
||||
type FeatureFlags = z.infer<typeof FeatureFlags>;
|
||||
|
||||
const readInMemoryFlagsFromLocalStorage = () => {
|
||||
const flags = savedRemoteFeatureFlags();
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
_state.isInternalUser = flags?.internalUser || isInternalUserViaEmail();
|
||||
};
|
||||
|
||||
const isInternalUserViaEmail = () => {
|
||||
const user = localUser();
|
||||
return !!user?.email.endsWith("@ente.io");
|
||||
};
|
||||
|
||||
/**
|
||||
* Return `true` if the current user is marked as an "internal" user.
|
||||
*
|
||||
* 1. Emails that end in `@ente.io` are considered as internal users.
|
||||
* 2. If the "internalUser" remote feature flag is set, the user is internal.
|
||||
* 3. Otherwise false.
|
||||
*/
|
||||
export const isInternalUser = () => _state.isInternalUser;
|
||||
@@ -10,13 +10,14 @@ import type { SetNotificationAttributes } from "./notification";
|
||||
*/
|
||||
export type AppContextT = AccountsContextT & {
|
||||
/**
|
||||
* Show the global activity indicator (a green bar at the top of the page).
|
||||
* Show the global activity indicator (a loading bar at the top of the
|
||||
* page).
|
||||
*/
|
||||
startLoading: () => void;
|
||||
showLoadingBar: () => void;
|
||||
/**
|
||||
* Hide the global activity indicator.
|
||||
* Hide the global activity indicator bar.
|
||||
*/
|
||||
finishLoading: () => void;
|
||||
hideLoadingBar: () => void;
|
||||
/**
|
||||
* Show a generic error dialog, and log the given error.
|
||||
*/
|
||||
|
||||
42
web/packages/new/photos/types/piexifjs.d.ts
vendored
42
web/packages/new/photos/types/piexifjs.d.ts
vendored
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Types for [piexifjs](https://github.com/hMatoba/piexifjs).
|
||||
*
|
||||
* Non exhaustive, only the function we need.
|
||||
*/
|
||||
declare module "piexifjs" {
|
||||
interface ExifObj {
|
||||
Exif?: Record<number, unknown>;
|
||||
}
|
||||
|
||||
interface Piexifjs {
|
||||
/**
|
||||
* Get exif data as object.
|
||||
*
|
||||
* @param jpegData a string that starts with "data:image/jpeg;base64,"
|
||||
* (a data URL), "\xff\xd8", or "Exif".
|
||||
*/
|
||||
load: (jpegData: string) => ExifObj;
|
||||
/**
|
||||
* Get exif as string to insert into JPEG.
|
||||
*
|
||||
* @param exifObj An object obtained using {@link load}.
|
||||
*/
|
||||
dump: (exifObj: ExifObj) => string;
|
||||
/**
|
||||
* Insert exif into JPEG.
|
||||
*
|
||||
* If {@link jpegData} is a data URL, returns the modified JPEG as a
|
||||
* data URL. Else if {@link jpegData} is binary as string, returns JPEG
|
||||
* as binary as string.
|
||||
*/
|
||||
insert: (exifStr: string, jpegData: string) => string;
|
||||
/**
|
||||
* Keys for the tags in {@link ExifObj}.
|
||||
*/
|
||||
ExifIFD: {
|
||||
DateTimeOriginal: number;
|
||||
};
|
||||
}
|
||||
const piexifjs: Piexifjs;
|
||||
export default piexifjs;
|
||||
}
|
||||
@@ -3602,11 +3602,6 @@ picomatch@^2.3.1:
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
||||
piexifjs@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/piexifjs/-/piexifjs-1.0.6.tgz#883811d73f447218d0d06e9ed7866d04533e59e0"
|
||||
integrity sha512-0wVyH0cKohzBQ5Gi2V1BuxYpxWfxF3cSqfFXfPIpl5tl9XLS5z4ogqhUCD20AbHi0h9aJkqXNJnkVev6gwh2ag==
|
||||
|
||||
pngjs@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821"
|
||||
@@ -3743,10 +3738,10 @@ react-is@^18.3.1:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
|
||||
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
|
||||
|
||||
react-otp-input@^2.3.1:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/react-otp-input/-/react-otp-input-2.4.0.tgz#0f0a3de1d8c8d564e2e4fbe5d6b7b56e29e3a6e6"
|
||||
integrity sha512-AIgl7u4sS9BTNCxX1xlaS5fPWay/Zml8Ho5LszXZKXrH1C/TiFsTQGmtl13UecQYO3mSF3HUzG2rrDf0sjEFmg==
|
||||
react-otp-input@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-otp-input/-/react-otp-input-3.1.1.tgz#910169629812c40a614e6c175cc2c5f36102bb61"
|
||||
integrity sha512-bjPavgJ0/Zmf/AYi4onj8FbH93IjeD+e8pWwxIJreDEWsU1ILR5fs8jEJmMGWSBe/yyvPP6X/W6Mk9UkOCkTPw==
|
||||
|
||||
react-refresh@^0.14.2:
|
||||
version "0.14.2"
|
||||
@@ -3768,7 +3763,7 @@ react-select@^5.8.0:
|
||||
react-transition-group "^4.3.0"
|
||||
use-isomorphic-layout-effect "^1.1.2"
|
||||
|
||||
react-top-loading-bar@^2.0.1:
|
||||
react-top-loading-bar@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-top-loading-bar/-/react-top-loading-bar-2.3.1.tgz#d727eb6aaa412eae52a990e5de9f33e9136ac714"
|
||||
integrity sha512-rQk2Nm+TOBrM1C4E3e6KwT65iXyRSgBHjCkr2FNja1S51WaPulRA5nKj/xazuQ3x89wDDdGsrqkqy0RBIfd0xg==
|
||||
|
||||
Reference in New Issue
Block a user