diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6bb853b994..64df70fb68 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -17,8 +17,8 @@ body: Please describe the bug. If possible, also include the steps to reproduce the behaviour, and the expected behaviour (sometimes bugs are just expectation mismatches, in which case this would be - a good fit for [feature - requests](https://github.com/ente-io/ente/discussions/categories/feature-requests)). + a good fit for + [enhancements](https://github.com/ente-io/ente/discussions/categories/enhancements)). validations: required: true - type: input @@ -33,12 +33,12 @@ body: The version where the feature was last known to be working. It is fine if you don't remember the exact version (mention roughly then), but if there just isn't a last known working version, then - it is likely that what is being reported is not an issue but a - feature request. The difference between the two categories is not - just semantic - feature requests use GitHub discussions and so can - be [upvoted by the - community](https://github.com/ente-io/ente/discussions/categories/feature-requests) - (issues can't be). + it is likely that what is being reported is not an issue + (regression) but an enhancement. The difference between the two + categories is not just semantic - **enhancements use GitHub + discussions and so can be [upvoted by the + community](https://github.com/ente-io/ente/discussions/categories/enhancements)** + (while issues cannot be). placeholder: e.g. v1.2.3 - type: dropdown attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4fae579410..cc9ab42e35 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: true contact_links: - - name: Feature requests and questions + - name: Enhacements, feature requests, feedback and questions url: https://github.com/ente-io/ente/discussions about: Please use Discussions for everything apart from the above. diff --git a/.github/workflows/auth-release.yml b/.github/workflows/auth-release.yml index 5bbff4a29c..97d079a681 100644 --- a/.github/workflows/auth-release.yml +++ b/.github/workflows/auth-release.yml @@ -83,7 +83,7 @@ jobs: # disable this step if release tag contains nightly or beta if: startsWith(github.ref, 'refs/tags/auth-v') && !contains(github.ref, 'nightly') && !contains(github.ref, 'beta') run: | - flutter build appbundle --release --flavor playstore --dart-define=app.flavor=playstore + flutter build appbundle --release --flavor playstore --dart-define=app.flavor=playstore --dart-define=cronetHttpNoPlay=true env: SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_auth_key.jks" SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} @@ -141,6 +141,7 @@ jobs: build-windows: runs-on: windows-latest + environment: "auth-win-build" defaults: run: @@ -174,14 +175,22 @@ jobs: - name: Retain Windows EXE and DLLs run: cp -r build/windows/x64/runner/Release ente-${{ github.ref_name }}-windows - - name: Code sign Windows installer and EXE - uses: dlemstra/code-sign-action@v1 + - name: Sign files with Trusted Signing + uses: azure/trusted-signing-action@v0 with: - certificate: "${{ secrets.WINDOWS_CERTIFICATE }}" - password: "${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}" - files: | - auth/artifacts/ente-${{ github.ref_name }}-installer.exe - auth/ente-${{ github.ref_name }}-windows/auth.exe + azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} + azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} + azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} + endpoint: ${{ secrets.AZURE_ENDPOINT }} + trusted-signing-account-name: ${{ secrets.AZURE_CODE_SIGNING_NAME }} + certificate-profile-name: ${{ secrets.AZURE_CERT_PROFILE_NAME }} + files: | + ${{ github.workspace }}/auth/artifacts/ente-${{ github.ref_name }}-installer.exe + ${{ github.workspace }}/auth/ente-${{ github.ref_name }}-windows/auth.exe + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + - name: Zip Windows EXE and DLLs run: tar.exe -a -c -f artifacts/ente-${{ github.ref_name }}-windows.zip ente-${{ github.ref_name }}-windows diff --git a/.github/workflows/auth-win-sign.yml b/.github/workflows/auth-win-sign.yml new file mode 100644 index 0000000000..860cde2ca2 --- /dev/null +++ b/.github/workflows/auth-win-sign.yml @@ -0,0 +1,70 @@ +name: "Windows build & Sign (auth)" + + +on: + workflow_dispatch: # Allow manually running the action + +env: + FLUTTER_VERSION: "3.24.3" + +permissions: + contents: write + +jobs: + build-windows: + runs-on: windows-latest + environment: "auth-win-build" + + defaults: + run: + working-directory: auth + + steps: + - name: Checkout code and submodules + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Flutter ${{ env.FLUTTER_VERSION }} + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Create artifacts directory + run: mkdir artifacts + + - name: Build Windows installer + run: | + flutter config --enable-windows-desktop + # dart pub global activate flutter_distributor + dart pub global activate --source git https://github.com/ente-io/flutter_distributor_fork --git-ref develop --git-path packages/flutter_distributor + make innoinstall + flutter_distributor package --platform=windows --targets=exe --skip-clean + mv dist/**/*-windows-setup.exe artifacts/ente-${{ github.ref_name }}-installer.exe + + - name: Retain Windows EXE and DLLs + run: cp -r build/windows/x64/runner/Release ente-${{ github.ref_name }}-windows + + - name: Sign files with Trusted Signing + uses: azure/trusted-signing-action@v0 + with: + azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} + azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} + azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} + endpoint: ${{ secrets.AZURE_ENDPOINT }} + trusted-signing-account-name: ${{ secrets.AZURE_CODE_SIGNING_NAME }} + certificate-profile-name: ${{ secrets.AZURE_CERT_PROFILE_NAME }} + files: | + ${{ github.workspace }}/auth/artifacts/ente-${{ github.ref_name }}-installer.exe + ${{ github.workspace }}/auth/ente-${{ github.ref_name }}-windows/auth.exe + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + + - name: Zip Windows EXE and DLLs + run: tar.exe -a -c -f artifacts/ente-${{ github.ref_name }}-windows.zip ente-${{ github.ref_name }}-windows + + - name: Generate checksums + run: sha256sum artifacts/ente-* > artifacts/sha256sum-windows diff --git a/.github/workflows/mobile-internal-release.yml b/.github/workflows/mobile-internal-release.yml index b8ef0b2225..f1d5724f97 100644 --- a/.github/workflows/mobile-internal-release.yml +++ b/.github/workflows/mobile-internal-release.yml @@ -63,6 +63,6 @@ jobs: with: webhook: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }} nodetail: true - title: "🏆 Internal release available for Photos" + title: "🏆 Internal release Photos (Branch: ${{ github.ref_name }})" description: "[Download](https://play.google.com/store/apps/details?id=io.ente.photos)" color: 0x00ff00 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 80e6424187..242cc2b65c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ Just hang around, enjoy the vibe. Answer someone's query on our [Discord](https://discord.gg/z2YVKkycX3), or pile on in the sporadic #off-topic rants there. Chuckle (or wince!) at our [Twitter](https://twitter.com/enteio) memes. Suggest a new feature in our [Github -Discussions](https://github.com/ente-io/ente/discussions/new?category=feature-requests), +Discussions](https://github.com/ente-io/ente/discussions/new?category=enhancements), or upvote the existing ones that you feel we should focus on first. Provide your opinion on existing threads. @@ -68,8 +68,8 @@ best to start small. Consider some well-scoped changes, say like adding more Each of the individual product/platform specific directories in this repository have instructions on setting up a dev environment. -For anything beyond trivial bug fixes, please use [features requests and -discussions](https://github.com/ente-io/ente/discussions) instead of performing +For anything beyond trivial bug fixes, please use +[discussions](https://github.com/ente-io/ente/discussions) instead of performing code changes directly. > [!TIP] diff --git a/auth/assets/custom-icons/_data/custom-icons.json b/auth/assets/custom-icons/_data/custom-icons.json index 9b64ae3d3b..dab6cd6adb 100644 --- a/auth/assets/custom-icons/_data/custom-icons.json +++ b/auth/assets/custom-icons/_data/custom-icons.json @@ -43,6 +43,12 @@ "title": "Anycoin Direct", "slug": "anycoindirect" }, + { + "title": "AR24", + "altNames": [ + "Docaposte AR24" + ] + }, { "title": "Aruba", "slug": "aruba", @@ -192,7 +198,11 @@ "slug": "blue_sky" }, { - "title": "bonify" + "title": "bonify", + "slug": "bonify", + "altNames": [ + "bonify.de" + ] }, { "title": "Booking", @@ -296,6 +306,15 @@ { "title": "CSAM" }, + { + "title": "CSSBuy", + "slug": "cssbuy", + "altNames": [ + "CSS Buy", + "CSS-Buy", + "cssbuy.com" + ] + }, { "title": "CSFloat" }, @@ -303,6 +322,10 @@ "title": "CSGORoll", "slug": "csgoroll" }, + { + "title": "Cryptee", + "slug": "cryptee" + }, { "title": "Cwallet", "altNames": [ @@ -435,6 +458,9 @@ "title": "Finanzfluss", "slug": "finanzfluss" }, + { + "title": "Finary" + }, { "title": "Firefox", "slug": "mozilla" @@ -456,6 +482,9 @@ "title": "Gate.io", "slug": "gateio.svg" }, + { + "title": "GERID" + }, { "title": "GitHub" }, @@ -737,6 +766,7 @@ { "title": "Mistral", "altNames": [ + "Le Chat", "Mistral AI", "MistralAI" ] @@ -886,6 +916,10 @@ "slug": "onshape", "hex": "7abb5e" }, + { + "title": "Oracle Cloud", + "slug": "oracle_cloud" + }, { "title": "Parqet", "slug": "parqet" @@ -1272,6 +1306,14 @@ "title": "US Mobile", "slug": "us_mobile" }, + { + "title": "uollet", + "slug": "uollet", + "altNames": [ + "UOLLET", + "uollet.com.br" + ] + }, { "title": "Vikunja" }, diff --git a/auth/assets/custom-icons/icons/ar24.svg b/auth/assets/custom-icons/icons/ar24.svg new file mode 100644 index 0000000000..31875c3048 --- /dev/null +++ b/auth/assets/custom-icons/icons/ar24.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/bonify.svg b/auth/assets/custom-icons/icons/bonify.svg index 0116be5044..2c12177f3a 100644 --- a/auth/assets/custom-icons/icons/bonify.svg +++ b/auth/assets/custom-icons/icons/bonify.svg @@ -1,29 +1,14 @@ - - - - - - - - - - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/cryptee.svg b/auth/assets/custom-icons/icons/cryptee.svg new file mode 100644 index 0000000000..1f2a5567ab --- /dev/null +++ b/auth/assets/custom-icons/icons/cryptee.svg @@ -0,0 +1 @@ + diff --git a/auth/assets/custom-icons/icons/cssbuy.svg b/auth/assets/custom-icons/icons/cssbuy.svg new file mode 100644 index 0000000000..62aeeb7eed --- /dev/null +++ b/auth/assets/custom-icons/icons/cssbuy.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/finary.svg b/auth/assets/custom-icons/icons/finary.svg new file mode 100644 index 0000000000..3e553974f5 --- /dev/null +++ b/auth/assets/custom-icons/icons/finary.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/gerid.svg b/auth/assets/custom-icons/icons/gerid.svg new file mode 100644 index 0000000000..c1f9593174 --- /dev/null +++ b/auth/assets/custom-icons/icons/gerid.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/auth/assets/custom-icons/icons/mistral.svg b/auth/assets/custom-icons/icons/mistral.svg index 1a6cd7fc03..3885997f7c 100644 --- a/auth/assets/custom-icons/icons/mistral.svg +++ b/auth/assets/custom-icons/icons/mistral.svg @@ -1,15 +1,7 @@ - - - - - - - - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/oracle_cloud.svg b/auth/assets/custom-icons/icons/oracle_cloud.svg new file mode 100644 index 0000000000..d7a2dbc5b7 --- /dev/null +++ b/auth/assets/custom-icons/icons/oracle_cloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/uollet.svg b/auth/assets/custom-icons/icons/uollet.svg new file mode 100644 index 0000000000..c767077239 --- /dev/null +++ b/auth/assets/custom-icons/icons/uollet.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/auth/lib/core/constants.dart b/auth/lib/core/constants.dart index b58d7dce61..44af78c841 100644 --- a/auth/lib/core/constants.dart +++ b/auth/lib/core/constants.dart @@ -11,7 +11,7 @@ const String roadmapURL = "https://roadmap.ente.io"; const String kAccountsUrl = "https://accounts.ente.io"; const String githubFeatureRequestUrl = - "https://github.com/ente-io/ente/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+requests%22+label%3A%22-+auth%22+sort%3Atop"; + "https://github.com/ente-io/ente/discussions/categories/enhancements?discussions_q=is%3Aopen%+label%3A%22-+auth%22+sort%3Atop"; const int microSecondsInDay = 86400000000; const int android11SDKINT = 30; const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748 diff --git a/auth/lib/gateway/authenticator.dart b/auth/lib/gateway/authenticator.dart index 3f3c7c8974..1375146e48 100644 --- a/auth/lib/gateway/authenticator.dart +++ b/auth/lib/gateway/authenticator.dart @@ -73,7 +73,10 @@ class AuthenticatorGateway { ); } - Future> getDiff(int sinceTime, {int limit = 500}) async { + Future<(List, int?)> getDiff( + int sinceTime, { + int limit = 500, + }) async { try { final response = await _enteDio.get( "/authenticator/entity/diff", @@ -84,11 +87,12 @@ class AuthenticatorGateway { ); final List authEntities = []; final diff = response.data["diff"] as List; + final int? unixTimeInMicroSeconds = response.data["timestamp"] as int?; for (var entry in diff) { final AuthEntity entity = AuthEntity.fromMap(entry); authEntities.add(entity); } - return authEntities; + return (authEntities, unixTimeInMicroSeconds); } catch (e) { if (e is DioException && e.response?.statusCode == 401) { throw UnauthorizedError(); diff --git a/auth/lib/l10n/arb/app_ar.arb b/auth/lib/l10n/arb/app_ar.arb index 7128f1b1e6..7872963dd8 100644 --- a/auth/lib/l10n/arb/app_ar.arb +++ b/auth/lib/l10n/arb/app_ar.arb @@ -47,7 +47,7 @@ "saveAction": "حفظ", "nextTotpTitle": "التالي", "deleteCodeTitle": "حذف الرمز؟", - "deleteCodeMessage": "هل أنت متأكد من أنك تريد حذف هذه الشيفرة؟ هذا الإجراء لا رجعة فيه.", + "deleteCodeMessage": "هل أنت متيقِّن من أنك تريد حذف هذه الشيفرة؟ هذا الإجراء لا رجعة فيه.", "trashCode": "حذف الكود؟", "trashCodeMessage": "هل أنت متيقِّن أنك تريد حذف الكود الخاص بـ {account}؟", "trash": "سلة المهملات", @@ -173,6 +173,7 @@ "invalidQRCode": "شيفرة استجابة سريعة غير صالحة", "noRecoveryKeyTitle": "لا يوجد مفتاح استرجاع؟", "enterEmailHint": "أدخل عنوان البريد الإلكتروني الخاص بك", + "enterNewEmailHint": "أدخل عنوان بريدك الإلكتروني الجديد", "invalidEmailTitle": "عنوان البريد الإلكتروني غير صالح", "invalidEmailMessage": "الرجاء إدخال بريد إلكتروني صالح.", "deleteAccount": "إزالة الحساب", @@ -513,5 +514,10 @@ "free5GB": "5GB مجانًا على ente صور", "loginWithAuthAccount": "سجّل الدخول باستخدام حساب المُصادقة", "freeStorageOffer": "خَصْم 10٪ على صور ente", - "freeStorageOfferDescription": "استخدم الكود \"AUTH\" وأحصل على 10٪ خَصْم في السنة الأولى" + "freeStorageOfferDescription": "استخدم الكود \"AUTH\" وأحصل على 10٪ خَصْم في السنة الأولى", + "advanced": "متقدم", + "algorithm": "الخوارزمية", + "type": "النوع", + "period": "المدّة", + "digits": "الأرقام" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_de.arb b/auth/lib/l10n/arb/app_de.arb index b39730d6ff..5e6ff47f98 100644 --- a/auth/lib/l10n/arb/app_de.arb +++ b/auth/lib/l10n/arb/app_de.arb @@ -173,6 +173,7 @@ "invalidQRCode": "Ungültiger QR-Code", "noRecoveryKeyTitle": "Kein Wiederherstellungsschlüssel?", "enterEmailHint": "Geben Sie Ihre E-Mail-Adresse ein", + "enterNewEmailHint": "Gib deine neue E-Mail-Adresse ein", "invalidEmailTitle": "Ungültige E-Mail-Adresse", "invalidEmailMessage": "Bitte geben Sie eine gültige E-Mail-Adresse ein.", "deleteAccount": "Konto löschen", @@ -513,5 +514,10 @@ "free5GB": "5GB kostenlos auf ente Photos", "loginWithAuthAccount": "Mit Ihrem Auth Account anmelden", "freeStorageOffer": "10% Rabatt für ente Photos", - "freeStorageOfferDescription": "Verwende den Code \"AUTH\", um 10% im ersten Jahr zu sparen" + "freeStorageOfferDescription": "Verwende den Code \"AUTH\", um 10% im ersten Jahr zu sparen", + "advanced": "Erweitert", + "algorithm": "Algorithmus", + "type": "Typ", + "period": "Periode", + "digits": "Ziffern" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_en.arb b/auth/lib/l10n/arb/app_en.arb index 64e0252bbe..6fc8f8ad55 100644 --- a/auth/lib/l10n/arb/app_en.arb +++ b/auth/lib/l10n/arb/app_en.arb @@ -173,6 +173,7 @@ "invalidQRCode": "Invalid QR code", "noRecoveryKeyTitle": "No recovery key?", "enterEmailHint": "Enter your email address", + "enterNewEmailHint": "Enter your new email address", "invalidEmailTitle": "Invalid email address", "invalidEmailMessage": "Please enter a valid email address.", "deleteAccount": "Delete account", diff --git a/auth/lib/l10n/arb/app_fr.arb b/auth/lib/l10n/arb/app_fr.arb index feb20b658b..4d0b461bed 100644 --- a/auth/lib/l10n/arb/app_fr.arb +++ b/auth/lib/l10n/arb/app_fr.arb @@ -173,6 +173,7 @@ "invalidQRCode": "QR code non valide", "noRecoveryKeyTitle": "Pas de clé de récupération ?", "enterEmailHint": "Entrez votre adresse e-mail", + "enterNewEmailHint": "Saisissez votre nouvelle adresse email", "invalidEmailTitle": "Adresse e-mail invalide", "invalidEmailMessage": "Veuillez saisir une adresse e-mail valide.", "deleteAccount": "Supprimer le compte", @@ -513,5 +514,10 @@ "free5GB": "5 Go gratuits sur Ente Photos", "loginWithAuthAccount": "Connectez-vous avec votre compte Auth", "freeStorageOffer": "10% de réduction sur Ente Photos", - "freeStorageOfferDescription": "Utilisez le code coupon \"AUTH\" pour obtenir 10% de réduction la première année" + "freeStorageOfferDescription": "Utilisez le code coupon \"AUTH\" pour obtenir 10% de réduction la première année", + "advanced": "Avancé", + "algorithm": "Algorithme", + "type": "Type", + "period": "Période", + "digits": "Chiffres" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_hu.arb b/auth/lib/l10n/arb/app_hu.arb index 8030313d85..68f8c804da 100644 --- a/auth/lib/l10n/arb/app_hu.arb +++ b/auth/lib/l10n/arb/app_hu.arb @@ -173,6 +173,7 @@ "invalidQRCode": "Érvénytelen QR-kód", "noRecoveryKeyTitle": "Nincs helyreállítási kulcs?", "enterEmailHint": "Adja meg az e-mail címét", + "enterNewEmailHint": "Add meg az új e-mail címed", "invalidEmailTitle": "Érvénytelen e-mail cím", "invalidEmailMessage": "Kérjük, adjon meg egy érvényes e-mail címet.", "deleteAccount": "Fiók törlése", @@ -513,5 +514,6 @@ "free5GB": "5GB ingyen ente Photos", "loginWithAuthAccount": "Jelentkezzen be Auth fiókjával", "freeStorageOffer": "10% kedvezmény on ente photos", - "freeStorageOfferDescription": "Használja az \"AUTH\" kódot, hogy 10% kedvezményt kapjon az első évben" + "freeStorageOfferDescription": "Használja az \"AUTH\" kódot, hogy 10% kedvezményt kapjon az első évben", + "type": "Típus" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_id.arb b/auth/lib/l10n/arb/app_id.arb index c791a866a9..b5e2605a3f 100644 --- a/auth/lib/l10n/arb/app_id.arb +++ b/auth/lib/l10n/arb/app_id.arb @@ -88,6 +88,8 @@ "useRecoveryKey": "Gunakan kunci pemulihan", "incorrectPasswordTitle": "Kata sandi salah", "welcomeBack": "Selamat datang kembali!", + "emailAlreadyRegistered": "Email sudah terdaftar.", + "emailNotRegistered": "Email belum terdaftar.", "madeWithLoveAtPrefix": "dibuat dengan ❤️ di ", "supportDevs": "Berlangganan ente untuk mendukung kami", "supportDiscount": "Gunakan kode kupon \"AUTH\" untuk mendapatkan potongan 10% untuk tahun pertama", @@ -171,6 +173,7 @@ "invalidQRCode": "Kode QR tidak valid", "noRecoveryKeyTitle": "Tidak punya kunci pemulihan?", "enterEmailHint": "Masukkan alamat email Anda", + "enterNewEmailHint": "Masukkan alamat email baru anda", "invalidEmailTitle": "Alamat email tidak valid", "invalidEmailMessage": "Harap masukkan alamat email yang valid.", "deleteAccount": "Hapus akun", @@ -501,5 +504,12 @@ "deselectAll": "Batalkan semua pilihan", "selectAll": "Pilih semua", "deleteDuplicates": "Hapus duplikat", - "plainHTML": "HTML Sederhana" + "plainHTML": "HTML Sederhana", + "tellUsWhatYouThink": "Berikan pendapatmu", + "dropReviewAndroid": "Berikan ulasan di Play Store", + "advanced": "Lanjutan", + "algorithm": "Algoritma", + "type": "Tipe", + "period": "Periode", + "digits": "Digit" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_it.arb b/auth/lib/l10n/arb/app_it.arb index c19e3077da..ae4b79c7a9 100644 --- a/auth/lib/l10n/arb/app_it.arb +++ b/auth/lib/l10n/arb/app_it.arb @@ -173,6 +173,7 @@ "invalidQRCode": "Codice QR non valido", "noRecoveryKeyTitle": "Nessuna chiave di recupero?", "enterEmailHint": "Inserisci il tuo indirizzo email", + "enterNewEmailHint": "Inserisci il tuo nuovo indirizzo email", "invalidEmailTitle": "Indirizzo email non valido", "invalidEmailMessage": "Inserisci un indirizzo email valido.", "deleteAccount": "Elimina account", @@ -513,5 +514,10 @@ "free5GB": "5GB gratis su ente Foto", "loginWithAuthAccount": "Accedi con il tuo account Auth", "freeStorageOffer": "10% di sconto su ente Foto", - "freeStorageOfferDescription": "Utilizzare il codice \"AUTH\" per ottenere il 10% di sconto al primo anno" + "freeStorageOfferDescription": "Utilizzare il codice \"AUTH\" per ottenere il 10% di sconto al primo anno", + "advanced": "Avanzate", + "algorithm": "Algoritmo", + "type": "Tipo", + "period": "Periodo", + "digits": "Cifre" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_lt.arb b/auth/lib/l10n/arb/app_lt.arb index babdd784ae..cd39c8b60f 100644 --- a/auth/lib/l10n/arb/app_lt.arb +++ b/auth/lib/l10n/arb/app_lt.arb @@ -508,9 +508,15 @@ "tellUsWhatYouThink": "Pasakykite mums, ką manote", "dropReviewiOS": "Rašyti apžvalgą parduotuvėje „App Store“", "dropReviewAndroid": "Rašyti apžvalgą parduotuvėje „Play“ parduotuvė“", + "supportEnte": "Paremti „ente“", "giveUsAStarOnGithub": "Suteikite mums žvaigždutę platformoje „Github“", "free5GB": "5 GB nemokami programai „ente“ nuotraukos", "loginWithAuthAccount": "Prisijungti su jūsų „Auth“ paskyra", "freeStorageOffer": "10 % nuolaida programai „ente“ nuotraukos", - "freeStorageOfferDescription": "Naudokite kodą „AUTH“, kad gautumėte 10 % nuolaida pirmiesiems metams. " + "freeStorageOfferDescription": "Naudokite kodą „AUTH“, kad gautumėte 10 % nuolaida pirmiesiems metams. ", + "advanced": "Išplėstiniai", + "algorithm": "Algoritmas", + "type": "Tipas", + "period": "Laikotarpis", + "digits": "Skaitmenys" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_nl.arb b/auth/lib/l10n/arb/app_nl.arb index 00ad14b4b5..125c0793b1 100644 --- a/auth/lib/l10n/arb/app_nl.arb +++ b/auth/lib/l10n/arb/app_nl.arb @@ -173,6 +173,7 @@ "invalidQRCode": "Ongeldige QR-code", "noRecoveryKeyTitle": "Geen herstelsleutel?", "enterEmailHint": "Voer je e-mailadres in", + "enterNewEmailHint": "Voer uw nieuwe e-mailadres in", "invalidEmailTitle": "Ongeldig e-mailadres", "invalidEmailMessage": "Voer een geldig e-mailadres in.", "deleteAccount": "Account verwijderen", @@ -513,5 +514,10 @@ "free5GB": "5GB gratis op ente Photos", "loginWithAuthAccount": "Log in met je Auth account", "freeStorageOffer": "10% korting op ente photos", - "freeStorageOfferDescription": "Gebruik de code \"AUTH\" voor 10% korting op je eerste jaar" + "freeStorageOfferDescription": "Gebruik de code \"AUTH\" voor 10% korting op je eerste jaar", + "advanced": "Geavanceerd", + "algorithm": "Algoritme", + "type": "Type", + "period": "Periode", + "digits": "Cijfers" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_pl.arb b/auth/lib/l10n/arb/app_pl.arb index 51ad435732..a4177c8133 100644 --- a/auth/lib/l10n/arb/app_pl.arb +++ b/auth/lib/l10n/arb/app_pl.arb @@ -173,6 +173,7 @@ "invalidQRCode": "Nieprawidłowy kod QR", "noRecoveryKeyTitle": "Brak klucza odzyskiwania?", "enterEmailHint": "Wprowadź adres e-mail", + "enterNewEmailHint": "Wprowadź nowy adres e-mail", "invalidEmailTitle": "Nieprawidłowy adres e-mail", "invalidEmailMessage": "Prosimy podać prawidłowy adres e-mail.", "deleteAccount": "Usuń konto", @@ -513,5 +514,10 @@ "free5GB": "5 GB za darmo na zdjęcia ente", "loginWithAuthAccount": "Zaloguj się przy użyciu konta Auth", "freeStorageOffer": "10% zniżki na zdjęcia ente", - "freeStorageOfferDescription": "Użyj kodu „AUTH”, aby uzyskać 10% zniżki na pierwszy rok" + "freeStorageOfferDescription": "Użyj kodu „AUTH”, aby uzyskać 10% zniżki na pierwszy rok", + "advanced": "Zaawansowane", + "algorithm": "Algorytm", + "type": "Rodzaj", + "period": "Okres", + "digits": "Cyfry" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_pt.arb b/auth/lib/l10n/arb/app_pt.arb index 2f542d5b82..0aff7e6cf8 100644 --- a/auth/lib/l10n/arb/app_pt.arb +++ b/auth/lib/l10n/arb/app_pt.arb @@ -173,6 +173,7 @@ "invalidQRCode": "QR Code inválido", "noRecoveryKeyTitle": "Sem chave de recuperação?", "enterEmailHint": "Insira o endereço de e-mail", + "enterNewEmailHint": "Insira seu novo e-mail", "invalidEmailTitle": "Endereço de e-mail inválido", "invalidEmailMessage": "Insira um endereço de e-mail válido.", "deleteAccount": "Excluir conta", @@ -513,5 +514,10 @@ "free5GB": "5GB grátis no ente Photos", "loginWithAuthAccount": "Registrar-se com sua conta Auth", "freeStorageOffer": "10% de desconto no ente photos", - "freeStorageOfferDescription": "Use o cupom \"AUTH\" para obter 10% de desconto no primeiro ano" + "freeStorageOfferDescription": "Use o cupom \"AUTH\" para obter 10% de desconto no primeiro ano", + "advanced": "Avançado", + "algorithm": "Algoritmo", + "type": "Tipo", + "period": "Período", + "digits": "Dígitos" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_sr.arb b/auth/lib/l10n/arb/app_sr.arb new file mode 100644 index 0000000000..dc461d9e89 --- /dev/null +++ b/auth/lib/l10n/arb/app_sr.arb @@ -0,0 +1,523 @@ +{ + "account": "Налог", + "unlock": "Откључај", + "recoveryKey": "Резервни Кључ", + "counterAppBarTitle": "Бројач", + "@counterAppBarTitle": { + "description": "Text shown in the AppBar of the Counter Page" + }, + "onBoardingBody": "Сигурносно правити копију 2ФА кôдова", + "onBoardingGetStarted": "Почети", + "setupFirstAccount": "Подесити свој први налог", + "importScanQrCode": "Скенирај QR кôд", + "qrCode": "QR кôд", + "importEnterSetupKey": "Унети кључ за подешавање", + "importAccountPageTitle": "Унети детаље налога", + "secretCanNotBeEmpty": "Тајна не може бити празна", + "bothIssuerAndAccountCanNotBeEmpty": "И издавалац и рачун не могу бити празни", + "incorrectDetails": "Погрешни детаљи", + "pleaseVerifyDetails": "Проверите детаље и покушајте поново", + "codeIssuerHint": "Издавач", + "codeSecretKeyHint": "Тајни кључ", + "secret": "Тајна", + "all": "Све", + "notes": "Белешке", + "notesLengthLimit": "Белешке могу имати највише {count} знакова", + "@notesLengthLimit": { + "description": "Text to indicate the maximum number of characters allowed for notes", + "placeholders": { + "count": { + "description": "The maximum number of characters allowed for notes", + "type": "int", + "example": "100" + } + } + }, + "codeAccountHint": "Налог (you@domain.com)", + "codeTagHint": "Ознака", + "accountKeyType": "Тип кључа", + "sessionExpired": "Сесија је истекла", + "@sessionExpired": { + "description": "Title of the dialog when the users current session is invalid/expired" + }, + "pleaseLoginAgain": "Молимо да се поново пријавите", + "loggingOut": "Одјављивање...", + "timeBasedKeyType": "Временски (TOTP)", + "counterBasedKeyType": "На основу бројања (HOTP)", + "saveAction": "Сачувај", + "nextTotpTitle": "следеће", + "deleteCodeTitle": "Обрисати кôд?", + "deleteCodeMessage": "Сигурно желите да избришете овај кôд? Ова акција је неповратна.", + "trashCode": "Кôд у смеће?", + "trashCodeMessage": "Сигурно желите да поставите кôд у смеће за {account}?", + "trash": "Смеће", + "viewLogsAction": "Прегледај извештаје", + "sendLogsDescription": "Ово ће делите ваше записе како би нам помогли да вам исправимо проблем. Док преузмемо мере предострожности да осигурамо да осетљиве информације нису пријављене, охрабрујемо вас да прегледате ове записе пре него што их делите.", + "preparingLogsTitle": "Спремање извештаја...", + "emailLogsTitle": "Имејловати извештаје", + "emailLogsMessage": "Пошаљите извештаје на {email}", + "@emailLogsMessage": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "copyEmailAction": "Копирати имејл", + "exportLogsAction": "Извези изештаје", + "reportABug": "Пријави грешку", + "crashAndErrorReporting": "Пријављивање дања и грешке", + "reportBug": "Пријaви грешку", + "emailUsMessage": "Пошаљите нам имејл на {email}", + "@emailUsMessage": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "contactSupport": "Контактирати подршку", + "rateUsOnStore": "Оцените нас на {storeName}", + "blog": "Блог", + "merchandise": "Роба", + "verifyPassword": "Верификујте лозинку", + "pleaseWait": "Молимо сачекајте...", + "generatingEncryptionKeysTitle": "Генерисање кључева за шифровање...", + "recreatePassword": "Поново креирати лозинку", + "recreatePasswordMessage": "Тренутни уређај није довољно моћан да потврди вашу лозинку, тако да је морамо да регенеришемо једном на начин који ради са свим уређајима. \n\nПријавите се помоћу кључа за опоравак и обновите своју лозинку (можете поново користити исту ако желите).", + "useRecoveryKey": "Користите кључ за опоравак", + "incorrectPasswordTitle": "Неисправна лозинка", + "welcomeBack": "Добродошли назад!", + "emailAlreadyRegistered": "Имејл је већ регистрован.", + "emailNotRegistered": "Имејл није регистрован.", + "madeWithLoveAtPrefix": "урађено са ❤️ на ", + "supportDevs": "Претплатити се на ente да би нас подржали", + "supportDiscount": "Употребите купон \"AUTH\" да би добили попуст од 10% прве године", + "changeEmail": "Промени имејл", + "changePassword": "Промени лозинку", + "data": "Подаци", + "importCodes": "Увоз кôдова", + "importTypePlainText": "Обичан текст", + "importTypeEnteEncrypted": "Ente шифрован извоз", + "passwordForDecryptingExport": "Лозинка за дешифровање извоза", + "passwordEmptyError": "Лозинка не може да буде празна", + "importFromApp": "Увоз кôдова од {appName}", + "importGoogleAuthGuide": "Извезите своје рачуне од Google Authenticator на QR кôд помоћу опције \"Трансфер налоге\". Затим помоћу другог уређаја скенирајте QR кôд.\n\nСавет: можете користити веб камеру вашег лаптопа да бисте снимили слику QR кôда.", + "importSelectJsonFile": "Одабрати JSON датотеку", + "importSelectAppExport": "Одабрати извозну датотеку {appName}-а", + "importEnteEncGuide": "Одабрати шифровану извозну JSON датотеку од Ente", + "importRaivoGuide": "Употребите \"Export OTPs to Zip archive\" опцију из подешавања Raivo-а.\n\nИздвојите zip датотеку и увезите JSON датотеку.", + "importBitwardenGuide": "Употребите \"Извоз Сефа\" из Bitwarden и увезите нешифровану JSON датотеку.", + "importAegisGuide": "Употребити \"Export the vault\" из Aegis-а.\n\nАко је сеф шифрован, мораћете унети лозинку сефа да би га дешифровали.", + "import2FasGuide": "Употребити \"Settings->Backup -Export\" из 2FAS-а.\n\nАко је ваша копија шифрирана, мораћете да унесете лозинку за дешифрирање копије", + "importLastpassGuide": "Употребити \"Transfer accounts\" из Lastpass Authenticator и стисните \"Export accounts to file\". Унесите преузет JSON.", + "exportCodes": "Извоз кôдова", + "importLabel": "Увоз", + "importInstruction": "Изаберите датотеку која садржи списак ваших кôдова у следећем формату", + "importCodeDelimiterInfo": "Кôдови се могу одвојити зарезом или новом линијом", + "selectFile": "Изаберите датотеку", + "emailVerificationToggle": "Имејл провера", + "emailVerificationEnableWarning": "Да бисте избегли да се закључате са свог рачуна, обавезно чувајте копију 2ФА имејла ван Ente Auth пре него што омогућите имејл верификацију.", + "authToChangeEmailVerificationSetting": "Потврдите аутентичност да промените верификацији имејл", + "authenticateGeneric": "Молимо потврдите аутентичност", + "authToViewYourRecoveryKey": "Аутентификујте се да бисте погледали кључ за опоравак", + "authToChangeYourEmail": "Аутентификујте се да бисте променили имејл", + "authToChangeYourPassword": "Аутентификујте се да бисте променили лозинку", + "authToViewSecrets": "Аутентификујте се да бисте прегледали Ваше тајне", + "authToInitiateSignIn": "Аутентификујте се да бисте почели пријављивање за копију.", + "ok": "У реду", + "cancel": "Откажи", + "yes": "Да", + "no": "Не", + "email": "Имејл", + "support": "Подршка", + "general": "Опште", + "settings": "Подешавања", + "copied": "Копирано", + "pleaseTryAgain": "Пробајте поново", + "existingUser": "Постојећи корисник", + "newUser": "Нов у Ente", + "delete": "Обриши", + "enterYourPasswordHint": "Унесите лозинку", + "forgotPassword": "Заборавио сам лозинку", + "oops": "Упс", + "suggestFeatures": "Предложи карактеристике", + "faq": "Питања", + "somethingWentWrongMessage": "Нешто је пошло наопако, покушајте поново", + "leaveFamily": "Напусти family претплату", + "leaveFamilyMessage": "Јесте ли сигурни да желите да напустите family чланство?", + "inFamilyPlanMessage": "Имате family чланство!", + "hintForMobile": "Дуго притисните кôд за уређивање или уклањање.", + "hintForDesktop": "Десни клик на кôд за уређивање или уклањање.", + "scan": "Скенирај", + "scanACode": "Скенирај кôд", + "verify": "Верификуј", + "verifyEmail": "Потврди имејл", + "enterCodeHint": "Унесите 6-цифрени кôд из\nапликације за аутентификацију", + "lostDeviceTitle": "Узгубили сте уређај?", + "twoFactorAuthTitle": "Дво-факторска аутентификација", + "passkeyAuthTitle": "Верификација сигурносном кључем", + "verifyPasskey": "Проверите сигурносни кључ", + "loginWithTOTP": "Пријава са TOTP", + "recoverAccount": "Опоравак налога", + "enterRecoveryKeyHint": "Унети кључ за опоравак", + "recover": "Опорави", + "contactSupportViaEmailMessage": "Послати имејл на {email} са регистрованог имејла", + "@contactSupportViaEmailMessage": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "invalidQRCode": "Неважећи QR кôд", + "noRecoveryKeyTitle": "Немате кључ за опоравак?", + "enterEmailHint": "Унесите Ваш имејл", + "enterNewEmailHint": "Унесите Ваш нови имејл", + "invalidEmailTitle": "Погрешна имејл адреса", + "invalidEmailMessage": "Унесите важећи имејл.", + "deleteAccount": "Избриши налог", + "deleteAccountQuery": "Жао нам је што одлазите. Да ли се суочавате са неком грешком?", + "yesSendFeedbackAction": "Да, послати повратне информације", + "noDeleteAccountAction": "Не, избрисати налог", + "initiateAccountDeleteTitle": "Молимо вас да се аутентификујете за брисање рачуна", + "sendEmail": "Шаљи имејл", + "createNewAccount": "Креирај нови налог", + "weakStrength": "Слабо", + "strongStrength": "Јако", + "moderateStrength": "Умерено", + "confirmPassword": "Потврдите лозинку", + "close": "Затвори", + "oopsSomethingWentWrong": "Нешто није у реду.", + "selectLanguage": "Изабери језик", + "language": "Језик", + "social": "Друштвене мреже", + "security": "Безбедност", + "lockscreen": "Закључавање екрана", + "authToChangeLockscreenSetting": "Аутентификујте се да бисте променили закључавање екрана", + "deviceLockEnablePreSteps": "Да бисте омогућили закључавање уређаја, молимо вас да подесите шифру уређаја или закључавање екрана у системским подешавањима.", + "viewActiveSessions": "Видети активне сесије", + "authToViewYourActiveSessions": "Аутентификујте се да бисте преглеадали активне сесије", + "searchHint": "Претрага...", + "search": "Претрага", + "sorryUnableToGenCode": "Извините, не могу да генеришем кôд за {issuerName}", + "noResult": "Нема резултата", + "addCode": "Додај кôд", + "scanAQrCode": "Скенирај QR кôд", + "enterDetailsManually": "Ручно унети детеље", + "edit": "Уреди", + "share": "Подели", + "shareCodes": "Дели кôдове", + "shareCodesDuration": "Изаберите трајање за које желите да поделите кôдове.", + "restore": "Врати", + "copiedToClipboard": "Копирано у оставу", + "copiedNextToClipboard": "Копирали следећи кôд у остави", + "error": "Грешка", + "recoveryKeyCopiedToClipboard": "Кључ за опоравак копирано у остави", + "recoveryKeyOnForgotPassword": "Ако заборавите лозинку, једини начин на који можете повратити податке је са овим кључем.", + "recoveryKeySaveDescription": "Не чувамо овај кључ, молимо да сачувате кључ од 24 речи на сигурном месту.", + "doThisLater": "Уради то касније", + "saveKey": "Сачувај кључ", + "save": "Сачувај", + "send": "Пошаљи", + "saveOrSendDescription": "Да ли желите да ово сачувате у складиште (фасцикли за преузимање подразумевано) или да га пошаљете другим апликацијама?", + "saveOnlyDescription": "Да ли желите да ово сачувате у складиште (фасцикли за преузимање подразумевано)?", + "back": "Назад", + "createAccount": "Направи налог", + "passwordStrength": "Снага лозинке: {passwordStrengthValue}", + "@passwordStrength": { + "description": "Text to indicate the password strength", + "placeholders": { + "passwordStrengthValue": { + "description": "The strength of the password as a string", + "type": "String", + "example": "Weak or Moderate or Strong" + } + }, + "message": "Password Strength: {passwordStrengthText}" + }, + "password": "Лозинка", + "signUpTerms": "Прихватам услове сервиса и политику приватности", + "privacyPolicyTitle": "Политика приватности", + "termsOfServicesTitle": "Услови", + "encryption": "Шифровање", + "setPasswordTitle": "Постави лозинку", + "changePasswordTitle": "Промени лозинку", + "resetPasswordTitle": "Ресетуј лозинку", + "encryptionKeys": "Кључеве шифровања", + "passwordWarning": "Не чувамо ову лозинку, па ако је заборавите, не можемо дешифрирати ваше податке", + "enterPasswordToEncrypt": "Унесите лозинку за употребу за шифровање ваших података", + "enterNewPasswordToEncrypt": "Унесите нову лозинку за употребу за шифровање ваших података", + "passwordChangedSuccessfully": "Лозинка је успешно промењена", + "generatingEncryptionKeys": "Генерисање кључева за шифровање...", + "continueLabel": "Настави", + "insecureDevice": "Уређај није сигуран", + "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Извините, не можемо да генеришемо сигурне кључеве на овом уређају.\n\nМолимо пријавите се са другог уређаја.", + "howItWorks": "Како то функционише", + "ackPasswordLostWarning": "Разумем да ако изгубим лозинку, могу изгубити своје податке пошто су шифрирани од краја до краја.", + "loginTerms": "Кликом на пријаву, прихватам услове сервиса и политику приватности", + "logInLabel": "Пријави се", + "logout": "Одјави ме", + "areYouSureYouWantToLogout": "Да ли сте сигурни да се одјавите?", + "yesLogout": "Да, одјави ме", + "exit": "Излаз", + "theme": "Тема", + "lightTheme": "Светла", + "darkTheme": "Tamna", + "systemTheme": "Систем", + "verifyingRecoveryKey": "Провера кључа за опоравак...", + "recoveryKeyVerified": "Кључ за опоравак је проверен", + "recoveryKeySuccessBody": "Сјајно! Ваш кључ за опоравак важи. Хвала за проверу.\n\nИмајте на уму да задржите кључ за опоравак на сигрном.", + "invalidRecoveryKey": "Кључ за опоравак који сте унели није валидан. Молимо вас да будете сигурни да садржи 24 речи и проверите правопис сваког.\n\nАко сте унели старији кôд за опоравак, проверите да ли је дугачак 64 знака и проверите сваки од њих.", + "recreatePasswordTitle": "Поново креирати лозинку", + "recreatePasswordBody": "Тренутни уређај није довољно моћан да потврди вашу лозинку, али можемо регенерирати на начин који ради са свим уређајима.\n\nПријавите се помоћу кључа за опоравак и обновите своју лозинку (можете поново користити исту ако желите).", + "invalidKey": "Неисправан кључ", + "tryAgain": "Покушај поново", + "viewRecoveryKey": "Видети кључ за опоравак", + "confirmRecoveryKey": "Потврдити кључ за опоравак", + "recoveryKeyVerifyReason": "Ваш кључ за опоравак је једини начин да се врате фотографије ако заборавите лозинку. Можете пронаћи свој кључ за опоравак у Подешавања> Рачун.\n\nОвдје унесите кључ за опоравак да бисте проверили да ли сте га исправно сачували.", + "confirmYourRecoveryKey": "Потврдити кључ за опоравак", + "confirm": "Потврди", + "emailYourLogs": "Имејлирајте извештаје", + "pleaseSendTheLogsTo": "Пошаљите извештаје на \n{toEmail}", + "copyEmailAddress": "Копирати имејл адресу", + "exportLogs": "Извези изештаје", + "enterYourRecoveryKey": "Унети кључ за опоравак", + "tempErrorContactSupportIfPersists": "Изгледа да је нешто погрешно. Покушајте поново након неког времена. Ако грешка настави, обратите се нашем тиму за подршку.", + "networkHostLookUpErr": "Није могуће повезивање са Ente-ом, молимо вас да проверите мрежне поставке и контактирајте подршку ако грешка и даље постоји.", + "networkConnectionRefusedErr": "Није могуће повезивање са Ente-ом, покушајте поново мало касније. Ако грешка настави, обратите се подршци.", + "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Изгледа да је нешто погрешно. Покушајте поново након неког времена. Ако грешка настави, обратите се нашем тиму за подршку.", + "about": "О програму", + "weAreOpenSource": "Користимо отворени извор!", + "privacy": "Приватност", + "terms": "Услови", + "checkForUpdates": "Провери ажурирања", + "checkStatus": "Провери статус", + "downloadUpdate": "Преузми", + "criticalUpdateAvailable": "Критично ажурирање је доступно", + "updateAvailable": "Доступно ажурирање", + "update": "Ажурирај", + "checking": "Провера...", + "youAreOnTheLatestVersion": "Користите најновију верзију", + "warning": "Упозорење", + "exportWarningDesc": "Извозна датотека садржи осетљиве информације. Молимо вас да је чувате на сигурно.", + "iUnderStand": "Разумем", + "@iUnderStand": { + "description": "Text for the button to confirm the user understands the warning" + }, + "authToExportCodes": "Аутентификујте се да бисте извезли кôдове", + "importSuccessTitle": "Jeeee!", + "importSuccessDesc": "Увели сте {count} кôдова!", + "@importSuccessDesc": { + "placeholders": { + "count": { + "description": "The number of codes imported", + "type": "int", + "example": "1" + } + } + }, + "sorry": "Жао ми је", + "importFailureDesc": "Нисам могао да анализирам изабрану датотеку.\nПишите на support@ente.io ако вам је потребна помоћ!", + "pendingSyncs": "Упозорење", + "pendingSyncsWarningBody": "Неки од ваших кôдова нису сачувани.\n\nМолимо вас осигурајте да имате резервну копију за ове кôдове пре него што се одјавите.", + "checkInboxAndSpamFolder": "Молимо вас да проверите примљену пошту (и нежељену пошту) да бисте довршили верификацију", + "tapToEnterCode": "Пипните да бисте унели кôд", + "resendEmail": "Поново послати имејл", + "weHaveSendEmailTo": "Послали смо имејл на {email}", + "@weHaveSendEmailTo": { + "description": "Text to indicate that we have sent a mail to the user", + "placeholders": { + "email": { + "description": "The email address of the user", + "type": "String", + "example": "example@ente.io" + } + } + }, + "manualSort": "Прилагођено", + "editOrder": "Уреди поредак", + "mostFrequentlyUsed": "Често коришћено", + "mostRecentlyUsed": "Недавно коришћено", + "activeSessions": "Активне сесије", + "somethingWentWrongPleaseTryAgain": "Нешто је пошло наопако. Покушајте поново", + "thisWillLogYouOutOfThisDevice": "Ово ће вас одјавити из овог уређаја!", + "thisWillLogYouOutOfTheFollowingDevice": "Ово ће вас одјавити из овог уређаја:", + "terminateSession": "Прекинути сесију?", + "terminate": "Прекини", + "thisDevice": "Овај уређај", + "toResetVerifyEmail": "Да бисте ресетовали лозинку, прво потврдите свој имејл.", + "thisEmailIsAlreadyInUse": "Овај имејл је већ у употреби", + "verificationFailedPleaseTryAgain": "Неуспешна верификација, покушајте поново", + "yourVerificationCodeHasExpired": "Ваш верификациони кôд је истекао", + "incorrectCode": "Погрешан кôд", + "sorryTheCodeYouveEnteredIsIncorrect": "Унет кôд није добар", + "emailChangedTo": "Имејл промењен на {newEmail}", + "authenticationFailedPleaseTryAgain": "Аутентификација није успела, покушајте поново", + "authenticationSuccessful": "Успешна аутентификација!", + "twofactorAuthenticationSuccessfullyReset": "Двофакторска аутентификација успешно рисетирана", + "incorrectRecoveryKey": "Нетачан кључ за опоравак", + "theRecoveryKeyYouEnteredIsIncorrect": "Унети кључ за опоравак је натачан", + "enterPassword": "Унеси лозинку", + "selectExportFormat": "Изабрати формат извоза", + "exportDialogDesc": "Шифровани извоз ће бити заштићен лозинком по вашем избору.", + "encrypted": "Шифровано", + "plainText": "Обичан текст", + "passwordToEncryptExport": "Лозинка за шифровање извоза", + "export": "Извези", + "useOffline": "Користите без резервних копија", + "signInToBackup": "Пријавите се да бисте сачували кôдове", + "singIn": "Пријавите се", + "sigInBackupReminder": "Извезите кôдове да бисте имали резервну копију од које можете да их вратите.", + "offlineModeWarning": "Одлучили сте да наставите без резервних копија. Молимо примите ручне резервне копије да бисте били сигурни да су ваше кодове на сигурном.", + "showLargeIcons": "Прикажи велике иконе", + "compactMode": "Компактни режим", + "shouldHideCode": "Сакриј кодове", + "doubleTapToViewHiddenCode": "Можете да двапут додирнете унос да бисте видели кôд", + "focusOnSearchBar": "Фокус на претрагу на покретање", + "confirmUpdatingkey": "Јесте ли сигурни да желите да ажурирате тајну кључ?", + "minimizeAppOnCopy": "Умањи апликацију после копије", + "editCodeAuthMessage": "Аутентификуј се за уред кôда", + "deleteCodeAuthMessage": "Аутентификуј се за брсање кôда", + "showQRAuthMessage": "Аутентификуј се за приказ QR кôда", + "confirmAccountDeleteTitle": "Потврда брисања рачуна", + "confirmAccountDeleteMessage": "Овај налог је повезан са другим Ente апликацијама, ако користите било коју.\n\nВаши преношени подаци, на свим Ente апликацијама биће заказани за брисање, и ваш рачун ће се трајно избрисати.", + "androidBiometricHint": "Потврдите идентитет", + "@androidBiometricHint": { + "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." + }, + "androidBiometricNotRecognized": "Нисмо препознали. Покушати поново.", + "@androidBiometricNotRecognized": { + "description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters." + }, + "androidBiometricSuccess": "Успех", + "@androidBiometricSuccess": { + "description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters." + }, + "androidCancelButton": "Откажи", + "@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": "Потребна аутентификација", + "@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." + }, + "androidBiometricRequiredTitle": "Потребна је биометрија", + "@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": "Потребни су акредитиви уређаја", + "@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": "Потребни су акредитиви уређаја", + "@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": "Иди на поставке", + "@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": "Биометријска аутентификација није постављена на вашем уређају. Идите на \"Подешавања> Сигурност\" да бисте додали биометријску аутентификацију.", + "@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": "Биометријска аутентификација је онемогућена. Закључајте и откључите екран да бисте је омогућили.", + "@iOSLockOut": { + "description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side." + }, + "iOSGoToSettingsDescription": "Биометријска аутентификација није постављена на вашем уређају. Молимо или омогућите Touch ID или Face ID.", + "@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." + }, + "iOSOkButton": "У реду", + "@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." + }, + "noInternetConnection": "Нема интернет везе", + "pleaseCheckYourInternetConnectionAndTryAgain": "Провери своју везу са интернетом и покушај поново.", + "signOutFromOtherDevices": "Одјави се из других уређаја", + "signOutOtherBody": "Ако мислиш да неко може знати твоју лозинку, можеш приморати одјављивање све остале уређаје које користе твој налог.", + "signOutOtherDevices": "Одјави друге уређаје", + "doNotSignOut": "Не одјави", + "hearUsWhereTitle": "Како сте чули о Ente? (опционо)", + "hearUsExplanation": "Не пратимо инсталацију апликације. Помогло би да нам кажеш како си нас нашао!", + "recoveryKeySaved": "Кључ за опоравак сачуван у фасцикли за преузимање!", + "waitingForBrowserRequest": "Чека се захтев за претраживач...", + "waitingForVerification": "Чека се верификација...", + "passkey": "Кључ за приступ", + "passKeyPendingVerification": "Верификација је још у току", + "loginSessionExpired": "Сесија је истекла", + "loginSessionExpiredDetails": "Ваша сесија је истекла. Молимо пријавите се поново.", + "developerSettingsWarning": "Сигурно желиш да промениш подешавања за програмере?", + "developerSettings": "Подешавања за програмере", + "serverEndpoint": "Крајња тачка сервера", + "invalidEndpoint": "Погрешна крајња тачка", + "invalidEndpointMessage": "Извини, крајња тачка коју си унео је неважећа. Унеси важећу крајњу тачку и покушај поново.", + "endpointUpdatedMessage": "Крајна тачка успешно ажурирана", + "customEndpoint": "Везано за {endpoint}", + "pinText": "Закачи", + "unpinText": "Откачи", + "pinnedCodeMessage": "{code} је прикачен", + "unpinnedCodeMessage": "{code} је одкачен", + "pinned": "Прикачено", + "tags": "Ознаке", + "createNewTag": "Креирај нову ознаку", + "tag": "Ознака", + "create": "Направи", + "editTag": "Уреди ознаку", + "deleteTagTitle": "Обрисати ознаку?", + "deleteTagMessage": "Сигурно желите да избришете ову ознаку? Ова акција је неповратна.", + "somethingWentWrongParsingCode": "Нисмо били у стању да рашчланимо {x} кôдова.", + "updateNotAvailable": "Ажурирање није доступно", + "viewRawCodes": "Погледајте сирове кôдове", + "rawCodes": "Сирове кôдове", + "rawCodeData": "Податак сировог кôда", + "appLock": "Закључавање апликације", + "noSystemLockFound": "Није пронађено ниједно закључавање система", + "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "Да бисте омогућили закључавање апликације, молимо вас да подесите шифру уређаја или закључавање екрана у системским подешавањима.", + "autoLock": "Ауто-закључавање", + "immediately": "Одмах", + "reEnterPassword": "Поново унеси лозинку", + "reEnterPin": "Поново унеси ПИН", + "next": "Следеће", + "tooManyIncorrectAttempts": "Превише погрешних покушаја", + "tapToUnlock": "Додирните да бисте откључали", + "setNewPassword": "Постави нову лозинку", + "deviceLock": "Закључавање уређаја", + "hideContent": "Сакриј садржај", + "hideContentDescriptionAndroid": "Сакрива садржај апликације у пребацивање апликација и онемогућује снимке екрана", + "hideContentDescriptioniOS": "Сакрива садржај апликације у пребацивање апликација", + "autoLockFeatureDescription": "Време након којег се апликација блокира након што је постављенеа у позадину", + "appLockDescription": "Изаберите између заданог закључавање екрана вашег уређаја и прилагођени екран за закључавање са ПИН-ом или лозинком.", + "pinLock": "ПИН клокирање", + "enterPin": "Унеси ПИН", + "setNewPin": "Постави нови ПИН", + "importFailureDescNew": "Није могао да анализира изабрану датотеку.", + "appLockNotEnabled": "Блокирање апликације није упаљено", + "appLockNotEnabledDescription": "Молимо упалие блокирање апликације на Безбедност > Блокирај апликацију", + "authToViewPasskey": "Аутентификујте се да бисте прегледали кључ", + "appLockOfflineModeWarning": "Одлучили сте да наставите без резервних копија. Ако заборавите лозинку, нећете моћи да приступите својим подацима.", + "duplicateCodes": "Дупликатни кодови", + "noDuplicates": "✨ Нема дупликата", + "youveNoDuplicateCodesThatCanBeCleared": "Немате дупликатне кодове који се могу очистити", + "deduplicateCodes": "Дедуплицирај кодове", + "deselectAll": "Поништи избор свега", + "selectAll": "Изабери све", + "deleteDuplicates": "Обриши дупликате", + "plainHTML": "HTML", + "tellUsWhatYouThink": "Реци нам шта мислиш", + "dropReviewiOS": "Напиши мишљење на App Store", + "dropReviewAndroid": "Напиши мишљење на Play Store", + "supportEnte": "Подржи ente", + "giveUsAStarOnGithub": "Дај нам звездицу на Github", + "free5GB": "5GB бесплатно на ente Photos", + "loginWithAuthAccount": "Пријави се са твојим Auth налогом", + "freeStorageOffer": "Попуст од 10% на ente photos", + "freeStorageOfferDescription": "Употребите кôд \"AUTH\" да би добили попуст од 10% прве године", + "advanced": "Напредно", + "algorithm": "Алгоритам", + "type": "Тип", + "period": "Период", + "digits": "Цифре" +} \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_tr.arb b/auth/lib/l10n/arb/app_tr.arb index 70b959f1ed..2500455576 100644 --- a/auth/lib/l10n/arb/app_tr.arb +++ b/auth/lib/l10n/arb/app_tr.arb @@ -9,7 +9,7 @@ "onBoardingBody": "2FA kodlarınızı güvenli bir şekilde yedekleyin", "onBoardingGetStarted": "Başlayın", "setupFirstAccount": "İlk hesabınızı ekleyin", - "importScanQrCode": "Karekod tara", + "importScanQrCode": "QR kod tara", "qrCode": "QR Kodu", "importEnterSetupKey": "Kurulum anahtarını giriniz", "importAccountPageTitle": "Hesap bilgilerinizi girin", @@ -142,7 +142,7 @@ "forgotPassword": "Şifremi unuttum", "oops": "Hay aksi", "suggestFeatures": "Özellik önerin", - "faq": "S.S.S.", + "faq": "SSS", "somethingWentWrongMessage": "Bir şeyler ters gitti, lütfen tekrar deneyin", "leaveFamily": "Aile planından ayrıl", "leaveFamilyMessage": "Aile planından ayrılmak istediğinize emin misiniz?", @@ -173,6 +173,7 @@ "invalidQRCode": "Geçersiz QR kodu", "noRecoveryKeyTitle": "Kurtarma anahtarınız yok mu?", "enterEmailHint": "E-posta adresinizi girin", + "enterNewEmailHint": "Yeni e-posta adresinizi girin", "invalidEmailTitle": "Geçersiz e-posta adresi", "invalidEmailMessage": "Lütfen geçerli bir e-posta adresi girin.", "deleteAccount": "Hesabı sil", @@ -513,5 +514,10 @@ "free5GB": "ente Fotoğraflarında 5GB ücretsiz", "loginWithAuthAccount": "Kimlik Doğrulama hesabınızla giriş yapın", "freeStorageOffer": "ente fotoğraflarında %10 indirim", - "freeStorageOfferDescription": "İlk yılda %10 indirim almak için \"AUTH\" kodunu kullanın" + "freeStorageOfferDescription": "İlk yılda %10 indirim almak için \"AUTH\" kodunu kullanın", + "advanced": "Gelişmiş", + "algorithm": "Algoritma", + "type": "Tür", + "period": "Zaman Aralığı", + "digits": "Uzunluk" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_zh_CN.arb b/auth/lib/l10n/arb/app_zh_CN.arb index a7c3a15e07..6301ceaf81 100644 --- a/auth/lib/l10n/arb/app_zh_CN.arb +++ b/auth/lib/l10n/arb/app_zh_CN.arb @@ -173,6 +173,7 @@ "invalidQRCode": "二维码无效", "noRecoveryKeyTitle": "没有恢复密钥吗?", "enterEmailHint": "请输入您的电子邮件地址", + "enterNewEmailHint": "请输入您的新电子邮件地址", "invalidEmailTitle": "无效的电子邮件地址", "invalidEmailMessage": "请输入一个有效的电子邮件地址。", "deleteAccount": "删除账户", @@ -513,5 +514,10 @@ "free5GB": "ente Photos 上 5GB 可用空间", "loginWithAuthAccount": "使用您的认证账户登录", "freeStorageOffer": "购买 ente Photos 可享受 10% 优惠", - "freeStorageOfferDescription": "使用优惠码“AUTH”可享受首年 10% 折扣" + "freeStorageOfferDescription": "使用优惠码“AUTH”可享受首年 10% 折扣", + "advanced": "高级", + "algorithm": "算法", + "type": "类型", + "period": "周期", + "digits": "数字" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_zh_TW.arb b/auth/lib/l10n/arb/app_zh_TW.arb index d02afb50d7..57734db276 100644 --- a/auth/lib/l10n/arb/app_zh_TW.arb +++ b/auth/lib/l10n/arb/app_zh_TW.arb @@ -513,5 +513,10 @@ "free5GB": "ente Photos 上 5GB 可用空間", "loginWithAuthAccount": "使用您的認證帳戶登錄", "freeStorageOffer": "購買 ente Photos 可享受 10% 優惠", - "freeStorageOfferDescription": "使用優惠碼“AUTH”可享受首年 10% 折扣" + "freeStorageOfferDescription": "使用優惠碼“AUTH”可享受首年 10% 折扣", + "advanced": "進階", + "algorithm": "演算法", + "type": "類型", + "period": "期間", + "digits": "數位" } \ No newline at end of file diff --git a/auth/lib/locale.dart b/auth/lib/locale.dart index 8f0e0659c3..03b9e294d2 100644 --- a/auth/lib/locale.dart +++ b/auth/lib/locale.dart @@ -38,28 +38,28 @@ const List appSupportedLocales = [ ]; Locale? autoDetectedLocale; -Locale localResolutionCallBack(locales, supportedLocales) { - Locale? languageCodeMatch; - final Map languageCodeToLocale = { - for (Locale supportedLocale in appSupportedLocales) - supportedLocale.languageCode: supportedLocale, - }; - - for (Locale locale in locales) { +// This function takes device locales and supported locales as input +// and returns the best matching locale. +// The device locales are sorted by priority, so the first one is the most preferred. +Locale localResolutionCallBack(onDeviceLocales, supportedLocales) { + final Set languageSupport = {}; + for (Locale supportedLocale in appSupportedLocales) { + languageSupport.add(supportedLocale.languageCode); + } + for (Locale locale in onDeviceLocales) { + // check if exact local is supported, if yes, return it if (appSupportedLocales.contains(locale)) { autoDetectedLocale = locale; return locale; } - - if (languageCodeMatch == null && - languageCodeToLocale.containsKey(locale.languageCode)) { - languageCodeMatch = languageCodeToLocale[locale.languageCode]; - autoDetectedLocale = languageCodeMatch; + // check if language code is supported, if yes, return it + if (languageSupport.contains(locale.languageCode)) { + autoDetectedLocale = locale; + return locale; } } - // Return the first language code match or default to 'en' - return languageCodeMatch ?? const Locale('en'); + return autoDetectedLocale ?? const Locale('en'); } Future getLocale({ diff --git a/auth/lib/services/authenticator_service.dart b/auth/lib/services/authenticator_service.dart index ee0ad32bd4..559e85f81a 100644 --- a/auth/lib/services/authenticator_service.dart +++ b/auth/lib/services/authenticator_service.dart @@ -13,6 +13,7 @@ import 'package:ente_auth/models/authenticator/auth_entity.dart'; import 'package:ente_auth/models/authenticator/auth_key.dart'; import 'package:ente_auth/models/authenticator/entity_result.dart'; import 'package:ente_auth/models/authenticator/local_auth_entity.dart'; +import 'package:ente_auth/services/preference_service.dart'; import 'package:ente_auth/store/authenticator_db.dart'; import 'package:ente_auth/store/offline_authenticator_db.dart'; import 'package:ente_crypto_dart/ente_crypto_dart.dart'; @@ -194,8 +195,13 @@ class AuthenticatorService { final int lastSyncTime = _prefs.getInt(_lastEntitySyncTime) ?? 0; _logger.info("Current sync is $lastSyncTime"); const int fetchLimit = 500; - final List result = + late final List result; + late final int? epochTimeInMicroseconds; + (result, epochTimeInMicroseconds) = await _gateway.getDiff(lastSyncTime, limit: fetchLimit); + PreferenceService.instance + .computeAndStoreTimeOffset(epochTimeInMicroseconds); + _logger.info("${result.length} entries fetched from remote"); if (result.isEmpty) { return; diff --git a/auth/lib/services/preference_service.dart b/auth/lib/services/preference_service.dart index 45a2cf0e66..773ee1b149 100644 --- a/auth/lib/services/preference_service.dart +++ b/auth/lib/services/preference_service.dart @@ -18,6 +18,7 @@ class PreferenceService { late final SharedPreferences _prefs; static const kHasShownCoachMarkKey = "has_shown_coach_mark_v2"; + static const kLocalTimeOffsetKey = "local_time_offset"; static const kShouldShowLargeIconsKey = "should_show_large_icons"; static const kShouldHideCodesKey = "should_hide_codes"; static const kShouldAutoFocusOnSearchBar = "should_auto_focus_on_search_bar"; @@ -114,4 +115,24 @@ class PreferenceService { return installedTimeinMillis; } } + + // localEpochOffsetInMilliSecond returns the local epoch offset in milliseconds. + // This is used to adjust the time for TOTP calculations when device local time is not in sync with actual time. + int timeOffsetInMilliSeconds() { + return _prefs.getInt(kLocalTimeOffsetKey) ?? 0; + } + + void computeAndStoreTimeOffset( + int? epochTimeInMicroseconds, + ) { + if (epochTimeInMicroseconds == null) { + _prefs.remove(kLocalTimeOffsetKey); + return; + } + int serverEpochTimeInMilliSecond = epochTimeInMicroseconds ~/ 1000; + int localEpochTimeInMilliSecond = DateTime.now().millisecondsSinceEpoch; + int localEpochOffset = + serverEpochTimeInMilliSecond - localEpochTimeInMilliSecond; + _prefs.setInt(kLocalTimeOffsetKey, localEpochOffset); + } } diff --git a/auth/lib/ui/account/change_email_dialog.dart b/auth/lib/ui/account/change_email_dialog.dart index 828970cd73..5277e0fcb6 100644 --- a/auth/lib/ui/account/change_email_dialog.dart +++ b/auth/lib/ui/account/change_email_dialog.dart @@ -18,7 +18,7 @@ class _ChangeEmailDialogState extends State { Widget build(BuildContext context) { final l10n = context.l10n; return AlertDialog( - title: Text(l10n.enterEmailHint), + title: Text(l10n.enterNewEmailHint), content: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, diff --git a/auth/lib/ui/code_timer_progress.dart b/auth/lib/ui/code_timer_progress.dart index 520a18d76c..58386fe438 100644 --- a/auth/lib/ui/code_timer_progress.dart +++ b/auth/lib/ui/code_timer_progress.dart @@ -7,10 +7,12 @@ import 'package:flutter/material.dart'; class CodeTimerProgress extends StatefulWidget { final int period; final bool isCompactMode; + final int timeOffsetInMilliseconds; const CodeTimerProgress({ super.key, required this.period, this.isCompactMode = false, + this.timeOffsetInMilliseconds = 0, }); @override @@ -20,7 +22,7 @@ class CodeTimerProgress extends StatefulWidget { class _CodeTimerProgressState extends State { late final Timer _timer; late final ValueNotifier _progress; - late final int _periodInMicros; + late final int _periodInMilii; // Reduce update frequency final int _updateIntervalMs = @@ -29,29 +31,30 @@ class _CodeTimerProgressState extends State { @override void initState() { super.initState(); - _periodInMicros = widget.period * 1000000; + _periodInMilii = widget.period * 1000; _progress = ValueNotifier(0.0); - _updateTimeRemaining(DateTime.now().microsecondsSinceEpoch); + _updateTimeRemaining(DateTime.now().millisecondsSinceEpoch); _timer = Timer.periodic(Duration(milliseconds: _updateIntervalMs), (timer) { - final now = DateTime.now().microsecondsSinceEpoch; + final now = DateTime.now().millisecondsSinceEpoch; _updateTimeRemaining(now); }); } - void _updateTimeRemaining(int currentMicros) { + void _updateTimeRemaining(int currentMilliSeconds) { // More efficient time calculation using modulo - final elapsed = (currentMicros) % _periodInMicros; - final timeRemaining = _periodInMicros - elapsed; - _progress.value = timeRemaining / _periodInMicros; + final elapsed = (currentMilliSeconds + widget.timeOffsetInMilliseconds) % + _periodInMilii; + final timeRemaining = _periodInMilii - elapsed; + _progress.value = timeRemaining / _periodInMilii; } @override void didUpdateWidget(covariant CodeTimerProgress oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.period != widget.period) { - _periodInMicros = widget.period * 1000000; - _updateTimeRemaining(DateTime.now().microsecondsSinceEpoch); + _periodInMilii = widget.period * 1000; + _updateTimeRemaining(DateTime.now().millisecondsSinceEpoch); } } diff --git a/auth/lib/ui/code_widget.dart b/auth/lib/ui/code_widget.dart index 3ccb7ab67b..be9c2b8a69 100644 --- a/auth/lib/ui/code_widget.dart +++ b/auth/lib/ui/code_widget.dart @@ -152,6 +152,8 @@ class _CodeWidgetState extends State { key: ValueKey('period_${widget.code.period}'), period: widget.code.period, isCompactMode: widget.isCompactMode, + timeOffsetInMilliseconds: + PreferenceService.instance.timeOffsetInMilliSeconds(), ), widget.isCompactMode ? const SizedBox(height: 4) diff --git a/auth/lib/utils/totp_util.dart b/auth/lib/utils/totp_util.dart index 6b7f53ae5b..53b68be5ff 100644 --- a/auth/lib/utils/totp_util.dart +++ b/auth/lib/utils/totp_util.dart @@ -1,8 +1,14 @@ import 'package:ente_auth/models/code.dart'; +import 'package:ente_auth/services/preference_service.dart'; import 'package:flutter/foundation.dart'; import 'package:otp/otp.dart' as otp; import 'package:steam_totp/steam_totp.dart'; +int millisecondsSinceEpoch() { + return DateTime.now().millisecondsSinceEpoch + + PreferenceService.instance.timeOffsetInMilliSeconds(); +} + String getOTP(Code code) { if (code.type == Type.steam || code.issuer.toLowerCase() == 'steam') { return _getSteamCode(code); @@ -12,7 +18,7 @@ String getOTP(Code code) { } return otp.OTP.generateTOTPCodeString( getSanitizedSecret(code.secret), - DateTime.now().millisecondsSinceEpoch, + millisecondsSinceEpoch(), length: code.digits, interval: code.period, algorithm: _getAlgorithm(code), @@ -34,7 +40,7 @@ String _getSteamCode(Code code, [bool isNext = false]) { final SteamTOTP steamtotp = SteamTOTP(secret: code.secret); return steamtotp.generate( - DateTime.now().millisecondsSinceEpoch ~/ 1000 + (isNext ? code.period : 0), + millisecondsSinceEpoch() ~/ 1000 + (isNext ? code.period : 0), ); } @@ -44,7 +50,7 @@ String getNextTotp(Code code) { } return otp.OTP.generateTOTPCodeString( getSanitizedSecret(code.secret), - DateTime.now().millisecondsSinceEpoch + code.period * 1000, + millisecondsSinceEpoch() + code.period * 1000, length: code.digits, interval: code.period, algorithm: _getAlgorithm(code), @@ -56,9 +62,7 @@ String getNextTotp(Code code) { // It returns the start time and a list of future codes. (int, List) generateFutureTotpCodes(Code code, int count) { final int startTime = - ((DateTime.now().millisecondsSinceEpoch ~/ 1000) ~/ code.period) * - code.period * - 1000; + ((millisecondsSinceEpoch() ~/ 1000) ~/ code.period) * code.period * 1000; final String secret = getSanitizedSecret(code.secret); final List codes = []; if (code.type == Type.steam || code.issuer.toLowerCase() == 'steam') { diff --git a/auth/linux/packaging/enteauth.appdata.xml b/auth/linux/packaging/enteauth.appdata.xml index b6682a60ce..6785961a91 100644 --- a/auth/linux/packaging/enteauth.appdata.xml +++ b/auth/linux/packaging/enteauth.appdata.xml @@ -18,6 +18,8 @@ + + @@ -33,4 +35,4 @@ #ffffff #000000 - \ No newline at end of file + diff --git a/auth/pubspec.lock b/auth/pubspec.lock index 1070e3ba96..fa632892e2 100644 --- a/auth/pubspec.lock +++ b/auth/pubspec.lock @@ -5,15 +5,15 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "76.0.0" + version: "72.0.0" _macros: dependency: transitive description: dart source: sdk - version: "0.3.3" + version: "0.3.2" adaptive_theme: dependency: "direct main" description: @@ -26,10 +26,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "6.7.0" ansicolor: dependency: transitive description: @@ -90,10 +90,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.11.0" auto_size_text: dependency: "direct main" description: @@ -130,10 +130,10 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" build: dependency: transitive description: @@ -202,10 +202,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.3.0" checked_yaml: dependency: transitive description: @@ -234,10 +234,10 @@ packages: dependency: transitive description: name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.1" code_builder: dependency: transitive description: @@ -250,10 +250,10 @@ packages: dependency: "direct main" description: name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.19.1" + version: "1.18.0" confetti: dependency: "direct main" description: @@ -435,10 +435,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.1" ffi: dependency: "direct main" description: @@ -944,18 +944,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -1024,18 +1024,18 @@ packages: dependency: transitive description: name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" url: "https://pub.dev" source: hosted - version: "0.1.3-main.0" + version: "0.1.2-main.4" matcher: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: @@ -1056,10 +1056,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.15.0" mime: dependency: transitive description: @@ -1168,10 +1168,10 @@ packages: dependency: "direct main" description: name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.9.0" path_drawing: dependency: transitive description: @@ -1472,7 +1472,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.0" + version: "0.0.99" sodium: dependency: transitive description: @@ -1509,10 +1509,10 @@ packages: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.10.1" + version: "1.10.0" sprintf: dependency: transitive description: @@ -1566,10 +1566,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.12.1" + version: "1.11.1" steam_totp: dependency: "direct main" description: @@ -1590,10 +1590,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -1606,10 +1606,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.2.0" styled_text: dependency: "direct main" description: @@ -1630,18 +1630,18 @@ packages: dependency: transitive description: name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.2" timezone: dependency: transitive description: @@ -1806,10 +1806,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "14.2.5" watcher: dependency: transitive description: @@ -1899,5 +1899,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.24.0" diff --git a/auth/pubspec.yaml b/auth/pubspec.yaml index cf2d97c7ec..bdb32b695b 100644 --- a/auth/pubspec.yaml +++ b/auth/pubspec.yaml @@ -1,7 +1,7 @@ name: ente_auth description: ente two-factor authenticator -version: 4.3.6+437 +version: 4.4.0+440 publish_to: none environment: diff --git a/auth/scripts/release_tag.sh b/auth/scripts/release_tag.sh new file mode 100755 index 0000000000..f35c89ab4d --- /dev/null +++ b/auth/scripts/release_tag.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +# Function to display usage +usage() { + echo "Usage: $0 tag" + exit 1 +} + +# Ensure a tag was provided +[[ $# -eq 0 ]] && usage + +# Exit immediately if a command exits with a non-zero status +set -e + +# Go to the project root directory +cd "$(dirname "$0")/.." + +# Get the tag from the command line argument +TAG=$1 + +# Define the appdata file path - use absolute path to avoid directory navigation issues +PROJECT_ROOT=$(pwd) +APPDATA_FILE="${PROJECT_ROOT}/linux/packaging/enteauth.appdata.xml" + +# Get the version from the pubspec.yaml file and cut everything after the + +VERSION=$(grep "^version:" pubspec.yaml | awk '{ print $2 }' | cut -d '+' -f 1) + +PREFIX="auth-v" + +# Ensure the tag has the correct prefix +if [[ $TAG != $PREFIX* ]]; then + echo "Invalid tag. tags must start with '$PREFIX'." + exit 1 +fi + +# Ensure the tag version is in the pubspec.yaml file +if [[ $TAG != *$VERSION ]]; then + echo "Invalid tag." + echo "The version $VERSION in pubspec doesn't match the version in tag $TAG" + exit 1 +fi + +# Extract version number from the tag (remove prefix) +TAG_VERSION=${TAG#$PREFIX} + +# Check if this version is already in the releases section of the appdata.xml file +if ! grep -q "/{print $0; print " "; next}1' "$APPDATA_FILE" > "${APPDATA_FILE}.tmp" + mv "${APPDATA_FILE}.tmp" "$APPDATA_FILE" + + echo "Added release entry for version $TAG_VERSION with date $TODAY" + + # Stage and commit the updated appdata.xml file + git add "$APPDATA_FILE" + git commit -m "Add release $TAG_VERSION to appdata.xml" + echo "Committed appdata.xml changes for version $TAG_VERSION" +fi + +# If all checks pass, create the tag +git tag $TAG +echo "Tag $TAG created." + +exit 0 \ No newline at end of file diff --git a/cli/internal/api/login.go b/cli/internal/api/login.go index 61491f1130..5706422516 100644 --- a/cli/internal/api/login.go +++ b/cli/internal/api/login.go @@ -83,13 +83,14 @@ func (c *Client) VerifySRPSession( return &res, nil } -func (c *Client) SendEmailOTP( +func (c *Client) SendLoginOTP( ctx context.Context, email string, ) error { var res AuthorizationResponse payload := map[string]interface{}{ - "email": email, + "email": email, + "purpose": "login", } r, err := c.restClient.R(). SetContext(ctx). diff --git a/cli/pkg/sign_in.go b/cli/pkg/sign_in.go index 4218c5b60b..7fb54738db 100644 --- a/cli/pkg/sign_in.go +++ b/cli/pkg/sign_in.go @@ -167,7 +167,7 @@ func (c *ClICtrl) verifyPassKey(ctx context.Context, authResp *api.Authorization } func (c *ClICtrl) validateEmail(ctx context.Context, email string) (*api.AuthorizationResponse, error) { - err := c.Client.SendEmailOTP(ctx, email) + err := c.Client.SendLoginOTP(ctx, email) if err != nil { return nil, err } diff --git a/desktop/CHANGELOG.md b/desktop/CHANGELOG.md index 4470e11864..a119f38c3e 100644 --- a/desktop/CHANGELOG.md +++ b/desktop/CHANGELOG.md @@ -1,9 +1,17 @@ # CHANGELOG -## v1.7.13 (Unreleased) +## v1.7.14 (Unreleased) + +- . + +## v1.7.13 + +- Generate streams for videos (beta) + + > Streamable videos can be enabled in Preferences. For more details, see the + > [video streaming FAQ](https://help.ente.io/photos/faq/video-streaming). - Support Turkish translations. -- . ## v1.7.12 diff --git a/desktop/package.json b/desktop/package.json index 63b79becc7..143e1b9160 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "ente", - "version": "1.7.13-beta", + "version": "1.7.14-beta", "private": true, "description": "Desktop client for Ente Photos", "repository": "github:ente-io/photos-desktop", @@ -38,25 +38,26 @@ "lru-cache": "^11.1.0", "next-electron-server": "^1.0.0", "node-stream-zip": "^1.15.0", - "onnxruntime-node": "^1.20.1" + "onnxruntime-node": "^1.20.1", + "zod": "^3.25.23" }, "devDependencies": { - "@eslint/js": "^9.25.1", - "@tsconfig/node22": "^22.0.1", + "@eslint/js": "^9.27.0", + "@tsconfig/node22": "^22.0.2", "@types/auto-launch": "^5.0.5", "@types/ffmpeg-static": "^3.0.3", "ajv": "^8.17.1", "concurrently": "^9.1.2", "cross-env": "^7.0.3", - "electron": "^36.1.0", + "electron": "^36.3.2", "electron-builder": "^26.0.14", "eslint": "^9", "prettier": "3.5.3", "prettier-plugin-organize-imports": "^4.1.0", - "prettier-plugin-packagejson": "^2.5.10", - "shx": "^0.3.4", + "prettier-plugin-packagejson": "^2.5.14", + "shx": "^0.4.0", "typescript": "^5.8.3", - "typescript-eslint": "^8.31.1" + "typescript-eslint": "^8.32.1" }, "packageManager": "yarn@1.22.22", "productName": "ente" diff --git a/desktop/src/main.ts b/desktop/src/main.ts index dd6cab4a18..687d8b11d6 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -78,6 +78,14 @@ export const allowWindowClose = (): void => { * We call this at the end of this file. */ const main = () => { + // Workaround for Electron 36 not launching on some Linux distros. Remove + // once fixed or otherwise mitigated upstream. + // + // https://github.com/electron/electron/issues/46538#issuecomment-2808806722 + if (process.platform == "linux") { + app.commandLine.appendSwitch("gtk-version", "3"); + } + const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { app.quit(); diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index b74b19ec6c..b4e7eff0ca 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -32,7 +32,7 @@ import { openLogDirectory, selectDirectory, } from "./services/dir"; -import { ffmpegExec } from "./services/ffmpeg"; +import { ffmpegDetermineVideoDuration, ffmpegExec } from "./services/ffmpeg"; import { fsExists, fsFindFiles, @@ -182,10 +182,10 @@ export const attachIPCHandlers = () => { "generateImageThumbnail", ( _, - dataOrPathOrZipItem: Uint8Array | string | ZipItem, + pathOrZipItem: string | ZipItem, maxDimension: number, maxSize: number, - ) => generateImageThumbnail(dataOrPathOrZipItem, maxDimension, maxSize), + ) => generateImageThumbnail(pathOrZipItem, maxDimension, maxSize), ); ipcMain.handle( @@ -193,9 +193,15 @@ export const attachIPCHandlers = () => { ( _, command: FFmpegCommand, - dataOrPathOrZipItem: Uint8Array | string | ZipItem, + pathOrZipItem: string | ZipItem, outputFileExtension: string, - ) => ffmpegExec(command, dataOrPathOrZipItem, outputFileExtension), + ) => ffmpegExec(command, pathOrZipItem, outputFileExtension), + ); + + ipcMain.handle( + "ffmpegDetermineVideoDuration", + (_, pathOrZipItem: string | ZipItem) => + ffmpegDetermineVideoDuration(pathOrZipItem), ); // - Upload diff --git a/desktop/src/main/services/ffmpeg-worker.ts b/desktop/src/main/services/ffmpeg-worker.ts index f6af11358c..a2a9ca1ab4 100644 --- a/desktop/src/main/services/ffmpeg-worker.ts +++ b/desktop/src/main/services/ffmpeg-worker.ts @@ -4,6 +4,7 @@ // See [Note: Using Electron APIs in UtilityProcess] about what we can and // cannot import. +import shellescape from "any-shell-escape"; import { expose } from "comlink"; import pathToFfmpeg from "ffmpeg-static"; import { randomBytes } from "node:crypto"; @@ -11,11 +12,16 @@ import fs_ from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import { Readable } from "node:stream"; +import { z } from "zod"; import type { FFmpegCommand } from "../../types/ipc"; import log from "../log-worker"; import { messagePortMainEndpoint } from "../utils/comlink"; -import { wait } from "../utils/common"; +import { nullToUndefined, wait } from "../utils/common"; import { execAsyncWorker } from "../utils/exec-worker"; +import { + authenticatedRequestHeaders, + publicRequestHeaders, +} from "../utils/http"; /* Ditto in the web app's code (used by the Wasm FFmpeg invocation). */ const ffmpegPathPlaceholder = "FFMPEG"; @@ -43,13 +49,21 @@ export interface FFmpegUtilityProcess { ffmpegGenerateHLSPlaylistAndSegments: ( inputFilePath: string, outputPathPrefix: string, - outputUploadURL: string, + fileID: number, + fetchURL: string, + authToken: string, ) => Promise; + + ffmpegDetermineVideoDuration: (inputFilePath: string) => Promise; } log.debugString("Started ffmpeg utility process"); +process.on("uncaughtException", (e, origin) => log.error(origin, e)); + process.parentPort.once("message", (e) => { + // Initialize ourselves with the data we got from our parent. + parseInitData(e.data); // Expose an instance of `FFmpegUtilityProcess` on the port we got from our // parent. expose( @@ -57,12 +71,30 @@ process.parentPort.once("message", (e) => { ffmpegExec, ffmpegConvertToMP4, ffmpegGenerateHLSPlaylistAndSegments, + ffmpegDetermineVideoDuration, } satisfies FFmpegUtilityProcess, messagePortMainEndpoint(e.ports[0]!), ); + // Let the main process know we're ready. mainProcess("ack", undefined); }); +/** + * We cannot access Electron's {@link app} object within a utility process, so + * we pass the value of `app.getVersion()` during initialization, and it can be + * subsequently retrieved from here. + */ +let _desktopAppVersion: string | undefined; + +/** Equivalent to `app.getVersion()` */ +const desktopAppVersion = () => _desktopAppVersion!; + +const FFmpegWorkerInitData = z.object({ appVersion: z.string() }); + +const parseInitData = (data: unknown) => { + _desktopAppVersion = FFmpegWorkerInitData.parse(data).appVersion; +}; + /** * Send a message to the main process using a barebones RPC protocol. */ @@ -180,6 +212,7 @@ export interface FFmpegGenerateHLSPlaylistAndSegmentsResult { playlistPath: string; dimensions: { width: number; height: number }; videoSize: number; + videoObjectID: string; } /** @@ -190,12 +223,12 @@ export interface FFmpegGenerateHLSPlaylistAndSegmentsResult { * * H.264, <= 10 MB - Skip * H.264, <= 4000 kb/s bitrate - Don't re-encode video stream - * BT.709, <= 2000 kb/s bitrate - Don't apply the scale+fps filter - * !BT.709 - Apply tonemap (zscale+tonemap+zscale) + * !HDR, <= 2000 kb/s bitrate - Don't apply the scale+fps filter + * HDR - Apply tonemap (zscale+tonemap+zscale) * * Example invocation: * - * ffmpeg -i in.mov -vf 'scale=-2:720,fps=30,zscale=transfer=linear,tonemap=tonemap=hable:desat=0,zscale=primaries=709:transfer=709:matrix=709,format=yuv420p' -c:v libx264 -c:a aac -f hls -hls_key_info_file out.m3u8.info -hls_list_size 0 -hls_flags single_file out.m3u8 + * ffmpeg -i in.mov -vf "scale=-2:'min(720,ih)',fps=30,zscale=transfer=linear,tonemap=tonemap=hable:desat=0,zscale=primaries=709:transfer=709:matrix=709,format=yuv420p" -c:v libx264 -c:a aac -f hls -hls_key_info_file out.m3u8.info -hls_list_size 0 -hls_flags single_file out.m3u8 * * See: [Note: Preview variant of videos] * @@ -206,9 +239,17 @@ export interface FFmpegGenerateHLSPlaylistAndSegmentsResult { * the user's local file system. This function will write the generated HLS * playlist and video segments under this prefix. * - * @returns The paths to two files on the user's local file system - one - * containing the generated HLS playlist, and the other containing the - * transcoded and encrypted video segments that the HLS playlist refers to. + * @param fileID The ID of the {@link EnteFile} whose HLS playlist we are + * generating. + * + * @param fetchURL The fully resolved API URL for obtaining pre-signed S3 URLs + * for uploading the generated video segment file. + * + * @param authToken A token that can be used to make API request to + * {@link fetchURL}. + * + * @returns The path to the file on the user's file system containing the + * generated HLS playlist, and other metadata about the generated video stream. * * If the video is such that it doesn't require stream generation, then this * function returns `undefined`. @@ -216,17 +257,21 @@ export interface FFmpegGenerateHLSPlaylistAndSegmentsResult { const ffmpegGenerateHLSPlaylistAndSegments = async ( inputFilePath: string, outputPathPrefix: string, - outputUploadURL: string, + fileID: number, + fetchURL: string, + authToken: string, ): Promise => { - const { isH264, isBT709, bitrate } = + const { isH264, isHDR, bitrate } = await detectVideoCharacteristics(inputFilePath); - log.debugString(JSON.stringify({ isH264, isBT709, bitrate })); + log.debugString(JSON.stringify({ isH264, isHDR, bitrate })); // If the video is smaller than 10 MB, and already H.264 (the codec we are // going to use for the conversion), then a streaming variant is not much // use. Skip such cases. // + // See also: [Note: Marking files which do not need video processing] + // // --- // // [Note: HEVC/H.265 issues] @@ -274,8 +319,10 @@ const ffmpegGenerateHLSPlaylistAndSegments = async ( // - BT.709 ("High-Definition" or HD) // - BT.2020 ("Ultra-High-Definition" or UHD, aka HDR^). // - // ^ HDR ("High-Dynamic-Range") is an addendum to BT.2020, but for our - // purpose here we can treat it as as alias. + // ^ HDR ("High-Dynamic-Range") is an addendum to BT.2020, but for the + // discussion here we can treat it as as alias. In particular, not all + // BT.2020 videos are HDR, the check we use instead looks for particular + // color transfers (see the `isHDRVideo` function below). // // BT.709 is the most common amongst these for older files out stored on // computers, and they conform mostly to the standard (one notable exception @@ -292,15 +339,14 @@ const ffmpegGenerateHLSPlaylistAndSegments = async ( // that uses the tonemap filter. // // However applying this tonemap to videos that are already HD leads to a - // brightness drop. So we conditionally apply this filter chain only if the - // colorspace is not already BT.709. + // brightness drop. So we conditionally apply this filter chain only if we + // can heuristically detect that the video is HDR. // - // See also: [Note: Alternative FFmpeg command for HDR videos], although - // that uses a allow-list based check (while here we use deny-list). + // See also: [Note: Alternative FFmpeg command for HDR videos]. // // Reference: // - https://trac.ffmpeg.org/wiki/colorspace - const tonemap = !isBT709; + const tonemap = isHDR; // We want the generated playlist to refer to the chunks as "output.ts". // @@ -318,6 +364,20 @@ const ffmpegGenerateHLSPlaylistAndSegments = async ( const playlistPath = path.join(outputPathPrefix, "output.m3u8"); const videoPath = path.join(outputPathPrefix, "output.ts"); + // A file into which we'll redirect ffmpeg's stderr. + // + // [Note: ERR_CHILD_PROCESS_STDIO_MAXBUFFER] + // + // For very large videos, the stderr output of ffmpeg may cause the stdio + // max buffer size limits to be exceeded, raising the following error: + // + // RangeError [ERR_CHILD_PROCESS_STDIO_MAXBUFFER]: stderr maxBuffer length exceeded + // + // So instead of capturing the stderr normally, we redirect it to a + // temporary file, and then read it from there to extract the video + // dimensions. + const stderrPath = path.join(outputPathPrefix, "stderr.txt"); + // Generate a cryptographically secure random key (16 bytes). const keyBytes = randomBytes(16); const keyB64 = keyBytes.toString("base64"); @@ -336,11 +396,23 @@ const ffmpegGenerateHLSPlaylistAndSegments = async ( // - the first line specifies the key URI that is written into the playlist. // - the second line specifies the path to the local file system file from // where ffmpeg should read the key. + // + // [Note: ffmpeg newlines] + // + // Tested on Windows that ffmpeg recognizes these lines correctly. In + // general, ffmpeg tends to expect input and write output the Unix way (\n), + // even when we're running on Windows. + // + // - The ffmetadata and the HLS playlist file generated by ffmpeg uses \n + // separators, even on Windows. + // - The HLS key info file, expected as an input by ffmpeg, works fine when + // \n separated even on Windows. + // const keyInfo = [keyURI, keyPath].join("\n"); // Overview: // - // - Video H.264 HD 720p 30fps. + // - Video H.264 HD 720p (max) 30fps. // - Audio AAC 128kbps. // - Encrypted HLS playlist with a single file containing all the chunks. // @@ -378,16 +450,15 @@ const ffmpegGenerateHLSPlaylistAndSegments = async ( // keeping aspect ratio and the calculated // dimension divisible by 2 (some of the other // operations require an even pixel count). - "scale=-2:720", + "scale=-2:'min(720,ih)'", // Convert the video to a constant 30 fps, // duplicating or dropping frames as necessary. "fps=30", ] : [], - // Convert the colorspace if the video is not in the HD - // color space (bt709). Before conversion, tone map colors - // so that they work the same across the change in the - // dyamic range. + // Convert the colorspace if the video is HDR. Before + // conversion, tone map colors so that they work the same + // across the change in the dyamic range. // // 1. The tonemap filter only works linear light, so we // first use zscale with transfer=linear to linearize @@ -453,8 +524,9 @@ const ffmpegGenerateHLSPlaylistAndSegments = async ( playlistPath, ].flat(); - let dimensions: ReturnType; + let dimensions: { width: number; height: number }; let videoSize: number; + let videoObjectID: string; try { // Write the key and the keyInfo to their desired paths. @@ -463,26 +535,45 @@ const ffmpegGenerateHLSPlaylistAndSegments = async ( fs.writeFile(keyInfoPath, keyInfo, { encoding: "utf8" }), ]); + // Tack on the redirection after constructing the command. + const commandWithRedirection = `${shellescape(command)} 2>${stderrPath}`; + // Run the ffmpeg command to generate the HLS playlist and segments. // // Note: Depending on the size of the input file, this may take long! - const { stderr: conversionStderr } = await execAsyncWorker(command); + await execAsyncWorker(commandWithRedirection); + + // While ffmpeg uses \n as the line separator in the generated playlist + // file on Windows too, add an extra safety check that should fail the + // HLS generation if this doesn't hold. See: [Note: ffmpeg newlines]. + if (process.platform == "win32") { + const playlistText = await fs.readFile(playlistPath, "utf-8"); + if (playlistText.includes("\r\n")) + throw new Error("Unexpected Windows newlines in playlist"); + } // Determine the dimensions of the generated video from the stderr // output produced by ffmpeg during the conversion. - dimensions = detectVideoDimensions(conversionStderr); + dimensions = await detectVideoDimensions(stderrPath); // Find the size of the generated video segments by reading the size of // the generated .ts file. videoSize = await fs.stat(videoPath).then((st) => st.size); - await uploadVideoSegments(videoPath, videoSize, outputUploadURL); + videoObjectID = await uploadVideoSegments( + videoPath, + videoSize, + fileID, + fetchURL, + authToken, + ); } catch (e) { log.error("HLS generation failed", e); await Promise.all([deletePathIgnoringErrors(playlistPath)]); throw e; } finally { await Promise.all([ + deletePathIgnoringErrors(stderrPath), deletePathIgnoringErrors(keyInfoPath), deletePathIgnoringErrors(keyPath), deletePathIgnoringErrors(videoPath), @@ -491,7 +582,7 @@ const ffmpegGenerateHLSPlaylistAndSegments = async ( ]); } - return { playlistPath, dimensions, videoSize }; + return { playlistPath, dimensions, videoSize, videoObjectID }; }; /** @@ -520,10 +611,10 @@ const deletePathIgnoringErrors = async (tempFilePath: string) => { * * Stream #0:1[0x2](und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p(progressive), 480x270 [SAR 1:1 DAR 16:9], 539 kb/s, 29.97 fps, 29.97 tbr, 30k tbn (default) */ -const videoStreamLineRegex = /Stream #.+: Video:(.+)\n/; +const videoStreamLineRegex = /Stream #.+: Video:(.+)\r?\n/; /** {@link videoStreamLineRegex}, but global. */ -const videoStreamLinesRegex = /Stream #.+: Video:(.+)\n/g; +const videoStreamLinesRegex = /Stream #.+: Video:(.+)\r?\n/g; /** * A regex that matches " kb/s" preceded by a space. See @@ -543,15 +634,16 @@ const videoDimensionsRegex = / ([1-9]\d*)x([1-9]\d*)/; interface VideoCharacteristics { isH264: boolean; - isBT709: boolean; + isHDR: boolean; bitrate: number | undefined; } + /** * Heuristically determine information about the video at the given * {@link inputFilePath}: * * - If is encoded using H.264 codec. - * - If it uses the BT.709 colorspace. + * - If it is HDR. * - Its bitrate. * * The defaults are tailored for the cases in which these conditions are used, @@ -586,13 +678,18 @@ const detectVideoCharacteristics = async (inputFilePath: string) => { // codec conversion to happen, even if it is unnecessary. const res: VideoCharacteristics = { isH264: false, - isBT709: false, + isHDR: false, bitrate: undefined, }; if (!videoStreamLine) return res; res.isH264 = videoStreamLine.startsWith("h264 "); - res.isBT709 = videoStreamLine.includes("bt709"); + + // Same check as `isHDRVideo`. + res.isHDR = + videoStreamLine.includes("smpte2084") || + videoStreamLine.includes("arib-std-b67"); + // The regex matches "\d kb/s", but there can be other units for the // bitrate. However, (a) "kb/s" is the most common for videos out in the // wild, and (b) even if we guess wrong it we'll just do "-v:c x264" instead @@ -600,7 +697,7 @@ const detectVideoCharacteristics = async (inputFilePath: string) => { const brs = videoBitrateRegex.exec(videoStreamLine)?.at(0); if (brs) { const br = parseInt(brs, 10); - if (br) res.bitrate = br; + if (br) res.bitrate = br * 1000; } return res; @@ -617,7 +714,12 @@ const detectVideoCharacteristics = async (inputFilePath: string) => { * * See: [Note: Parsing CLI output might break on ffmpeg updates]. */ -const detectVideoDimensions = (conversionStderr: string) => { +const detectVideoDimensions = async (stderrPath: string) => { + // Instead of reading the stderr directly off the child_process.exec, we + // wrote it to a file to avoid hitting the max stdio buffer limits. Read it + // from there. + const conversionStderr = await fs.readFile(stderrPath, "utf-8"); + // There is a nicer way to do it - by running `pseudoFFProbeVideo` on the // generated playlist. However, that playlist includes a data URL that // specifies the encryption info, and ffmpeg refuses to read that unless we @@ -657,22 +759,21 @@ const detectVideoDimensions = (conversionStderr: string) => { * Heuristically detect if the file at given path is a HDR video. * * This is similar to {@link detectVideoCharacteristics}, and see that - * function's documentation for all the caveats. However, this function uses an - * allow-list instead, and considers any file with color transfer "smpte2084" or - * "arib-std-b67" to be HDR. While this is in some sense a more exact check, it - * comes with different caveats: + * function's documentation for all the caveats. Specifically, this function + * uses an allow-list, and considers any file with color transfer "smpte2084" or + * "arib-std-b67" to be HDR. Caveats: * - * - These particular constants are not guaranteed to be correct; these are just - * what I saw on the internet as being used / recommended for detecting HDR. + * 1. These particular constants are not guaranteed to be correct; these are + * from various internet posts as being used / recommended for detecting HDR. * - * - Since we don't have ffprobe, we're not checking the color space value - * itself but a substring of the stream line in the ffmpeg stderr output. + * 2. Since we don't have ffprobe, we're not checking the color space value + * itself but a substring of the stream line in the ffmpeg stderr output. * - * In particular, we use this more exact check for places where we have less - * leeway. e.g. when generating thumbnails, if we apply the tonemapping to any - * non-BT.709 file (as the HLS stream generation does), we start getting the - * "code 3074: no path between colorspaces" error during the JPEG conversion - * (this is not a problem in the H.264 conversion). + * This check should generally not have false positives (unless something else + * in the log line triggers #2), but it can have false negative. This is the + * lesser of the two evils since if we apply the tonemapping to any non-BT.709 + * file, we start getting the "code 3074: no path between colorspaces" error + * during the JPEG or H.264 conversion. * * - See: [Note: Alternative FFmpeg command for HDR videos] * - See: [Note: Tonemapping HDR to HD] @@ -728,10 +829,11 @@ const pseudoFFProbeVideo = async (inputFilePath: string) => { }; /** - * Upload the file at the given {@link videoFilePath} to the provided presigned - * {@link objectUploadURL} using a HTTP PUT request. + * Upload the file at the given {@link videoFilePath} to the provided pre-signed + * URL(s) using a HTTP PUT request. * - * In case on non-HTTP-4xx errors, retry up to 3 times with exponential backoff. + * All HTTP requests are retried up to 4 times (1 original + 3 retries) with + * exponential backoff. * * See: [Note: Upload HLS video segment from node side]. * @@ -740,20 +842,155 @@ const pseudoFFProbeVideo = async (inputFilePath: string) => { * * @param videoSize The size in bytes of the file at {@link videoFilePath}. * - * @param objectUploadURL A pre-signed URL to upload the file. + * @param fileID The ID of the {@link EnteFile} whose video segment this is. * - * --- + * @param fetchURL The API URL for fetching pre-signed upload URLs. * - * This is an inlined but bespoke reimplementation of `retryEnsuringHTTPOkOr4xx` - * from `web/packages/base/http.ts` + * @param authToken The user's auth token for use with {@link fetchURL}. + * + * @return The object ID of the uploaded file on remote storage. + */ +const uploadVideoSegments = async ( + videoFilePath: string, + videoSize: number, + fileID: number, + fetchURL: string, + authToken: string, +) => { + // Self hosters might be using Cloudflare's free plan which (currently) has + // a maximum request size of 100 MB. Keeping a bit of margin for headers, + const partSize = 96 * 1024 * 1024; /* 96 MB */ + const partCount = Math.ceil(videoSize / partSize); + + const { objectID, url, partURLs, completeURL } = + await getFilePreviewDataUploadURL( + partCount, + fileID, + fetchURL, + authToken, + ); + + if (url) { + await uploadVideoSegmentsSingle(videoFilePath, videoSize, url); + } else if (partURLs && completeURL) { + await uploadVideoSegmentsMultipart( + videoFilePath, + videoSize, + partSize, + partURLs, + completeURL, + ); + } else { + throw new Error("Malformed upload URLs"); + } + + return objectID; +}; + +const FilePreviewDataUploadURLResponse = z.object({ + /** + * The objectID with which this uploaded data can be referred to post upload + * (e.g. when invoking {@link putVideoData}). + */ + objectID: z.string(), + /** + * A pre-signed URL that can be used to upload the file. + * + * This will be present only if we requested a singular object upload URL. + */ + url: z.string().nullish().transform(nullToUndefined), + /** + * A list of pre-signed URLs that can be used to upload parts of a multipart + * upload of the uploaded data. + * + * This will be present only if we requested a multipart upload URLs for the + * object by setting `isMultiPart` true in the request. + */ + partURLs: z.string().array().nullish().transform(nullToUndefined), + /** + * A pre-signed URL that can be used to finalize the multipart upload. + * + * This will be present only if we requested a multipart upload URLs for the + * object by setting `isMultiPart` true in the request. + */ + completeURL: z.string().nullish().transform(nullToUndefined), +}); + +/** + * Obtain a pre-signed URL(s) that can be used to upload the "file preview data" + * of type "vid_preview". + * + * This will be the file containing the encrypted video segments which the + * "vid_preview" HLS playlist for the file would refer to. + * + * @param partCount If greater than 1, then we request for a multipart upload. + */ +export const getFilePreviewDataUploadURL = async ( + partCount: number, + fileID: number, + fetchURL: string, + authToken: string, +) => { + const params = new URLSearchParams({ + fileID: fileID.toString(), + type: "vid_preview", + }); + if (partCount > 1) { + params.set("isMultiPart", "true"); + params.set("count", partCount.toString()); + } + + const res = await retryEnsuringHTTPOk(() => + fetch(`${fetchURL}?${params.toString()}`, { + headers: authenticatedRequestHeaders( + desktopAppVersion(), + authToken, + ), + }), + ); + + return FilePreviewDataUploadURLResponse.parse(await res.json()); +}; + +const uploadVideoSegmentsSingle = ( + videoFilePath: string, + videoSize: number, + objectUploadURL: string, +) => + retryEnsuringHTTPOk(() => + // net.fetch is 40-50x slower than the native fetch for this particular + // PUT request. This is easily reproducible - replace `fetch` with + // `net.fetch`, then even on localhost the PUT requests start taking a + // minute or so, while they take second(s) with node's native fetch. + fetch(objectUploadURL, { + method: "PUT", + // net.fetch deduces and inserts a content-length for us, when we + // use the node native fetch then we need to provide it explicitly. + headers: { + ...publicRequestHeaders(desktopAppVersion()), + "Content-Length": `${videoSize}`, + }, + // See: [Note: duplex param required for stream body] + // @ts-expect-error ^see note above + duplex: "half", + body: Readable.toWeb(fs_.createReadStream(videoFilePath)), + }), + ); + +/** + * Retry a async operation on failure up to 4 times (1 original + 3 retries) + * with exponential backoff. + * + * This is an inlined but bespoke reimplementation of `retryEnsuringHTTPOk` from + * `web/packages/base/http.ts` * * - We don't have the rest of the scaffolding used by that function, which is * why it is intially inlined bespoked. * * - It handles the specific use case of uploading videos since generating the * HLS stream is a fairly expensive operation, so a retry to discount - * transient network issues is called for. There are only 2 retries for a - * total of 3 attempts, and the retry gaps are more spaced out. + * transient network issues is called for. The number of retries and their + * gaps are same as the "background" `retryProfile` of the web implementation. * * - Later it was discovered that net.fetch is much slower than node's native * fetch, so this implementation has further diverged. @@ -761,61 +998,136 @@ const pseudoFFProbeVideo = async (inputFilePath: string) => { * - This also moved to a utility process, where we also have a more restricted * ability to import electron API. */ -const uploadVideoSegments = async ( - videoFilePath: string, - videoSize: number, - objectUploadURL: string, -) => { - const waitTimeBeforeNextTry = [5000, 20000]; +const retryEnsuringHTTPOk = async (request: () => Promise) => { + const waitTimeBeforeNextTry = [10000, 30000, 120000]; while (true) { - let abort = false; try { - const nodeStream = fs_.createReadStream(videoFilePath); - const webStream = Readable.toWeb(nodeStream); - - // net.fetch is 40-50x slower than the native fetch for this - // particular PUT request. This is easily reproducible - replace - // `fetch` with `net.fetch`, then even on localhost the PUT requests - // start taking a minute or so, while they take second(s) with - // node's native fetch. - const res = await fetch(objectUploadURL, { - method: "PUT", - // net.fetch apparently deduces and inserts a content-length, - // because when we use the node native fetch then we need to - // provide it explicitly. - headers: { "Content-Length": `${videoSize}` }, - // The duplex option is required since we're passing a stream. - // - // @ts-expect-error TypeScript's libdom.d.ts does not include - // the "duplex" parameter, e.g. see - // https://github.com/node-fetch/node-fetch/issues/1769. - duplex: "half", - body: webStream, - }); - - if (res.ok) { - // Success. - return; - } - if (res.status >= 400 && res.status < 500) { - // HTTP 4xx. - abort = true; - } + const res = await request(); + if (res.ok) /* Success. */ return res; throw new Error( - `Failed to upload generated HLS video: HTTP ${res.status} ${res.statusText}`, + `Request failed: HTTP ${res.status} ${res.statusText}`, ); } catch (e) { - if (abort) { - throw e; - } const t = waitTimeBeforeNextTry.shift(); if (!t) { throw e; } else { log.warn("Will retry potentially transient request failure", e); + await wait(t); } - await wait(t); } } }; + +const uploadVideoSegmentsMultipart = async ( + videoFilePath: string, + videoSize: number, + partSize: number, + partUploadURLs: string[], + completionURL: string, +) => { + // The part we're currently uploading. + let partNumber = 0; + // A rolling offset into the file. + let start = 0; + // See `createMultipartUploadRequestBody` in the web code for a more + // expansive and documented version of this XML body construction. + const completionXML = [""]; + for (const partUploadURL of partUploadURLs) { + partNumber += 1; + const size = Math.min(start + partSize, videoSize) - start; + const end = start + size - 1; + const res = await retryEnsuringHTTPOk(() => + fetch(partUploadURL, { + method: "PUT", + headers: { + ...publicRequestHeaders(desktopAppVersion()), + "Content-Length": `${size}`, + }, + // See: [Note: duplex param required for stream body] + // @ts-expect-error ^see note above + duplex: "half", + body: Readable.toWeb( + // start and end are inclusive 0-indexed range of bytes to + // read from the file. + fs_.createReadStream(videoFilePath, { start, end }), + ), + }), + ); + const eTag = res.headers.get("etag"); + if (!eTag) throw new Error("Response did not have an ETag"); + start += size; + completionXML.push( + `${partNumber}${eTag}`, + ); + } + completionXML.push(""); + const completionBody = completionXML.join(""); + return await retryEnsuringHTTPOk(() => + fetch(completionURL, { + method: "POST", + headers: { + ...publicRequestHeaders(desktopAppVersion()), + "Content-Type": "text/xml", + }, + body: completionBody, + }), + ); +}; + +/** + * A regex that matches the first line of the form + * + * Duration: 00:00:03.13, start: 0.000000, bitrate: 16088 kb/s + * + * The part after Duration: and until the first non-digit or colon is the first + * capture group, while after the dot is an optional second capture group. + */ +const videoDurationLineRegex = /\s\sDuration: ([0-9:]+)(.[0-9]+)?/; + +/** + * Determine the duration of the video at the given {@link inputFilePath}. + * + * While the detection works for all known cases, it is still heuristic because + * it uses ffmpeg output instead of ffprobe (which we don't have access to). + * See: [Note: Parsing CLI output might break on ffmpeg updates]. + */ +export const ffmpegDetermineVideoDuration = async (inputFilePath: string) => { + const videoInfo = await pseudoFFProbeVideo(inputFilePath); + const matches = videoDurationLineRegex.exec(videoInfo); + + const fail = () => { + throw new Error(`Cannot parse video duration '${matches?.at(0)}'`); + }; + + // The HH:mm:ss. + const ints = (matches?.at(1) ?? "") + .split(":") + .map((s) => parseInt(s, 10) || 0); + let [h, m, s] = [0, 0, 0]; + switch (ints.length) { + case 1: + s = ints[0]!; + break; + case 2: + m = ints[0]!; + s = ints[1]!; + break; + case 3: + h = ints[0]!; + m = ints[1]!; + s = ints[2]!; + break; + default: + fail(); + } + + // Optional subseconds. + const ss = parseFloat(`0${matches?.at(2) ?? ""}`); + + // Follow the same round up behaviour that the web side uses. + const duration = Math.ceil(h * 3600 + m * 60 + s + ss); + if (!duration) fail(); + return duration; +}; diff --git a/desktop/src/main/services/ffmpeg.ts b/desktop/src/main/services/ffmpeg.ts index 1b0e623faa..f64a721a05 100644 --- a/desktop/src/main/services/ffmpeg.ts +++ b/desktop/src/main/services/ffmpeg.ts @@ -8,7 +8,7 @@ import fs from "node:fs/promises"; import type { FFmpegCommand, ZipItem } from "../../types/ipc"; import { deleteTempFileIgnoringErrors, - makeFileForDataOrStreamOrPathOrZipItem, + makeFileForStreamOrPathOrZipItem, makeTempFilePath, } from "../utils/temp"; import type { FFmpegUtilityProcess } from "./ffmpeg-worker"; @@ -29,27 +29,49 @@ export const ffmpegUtilityProcess = () => */ export const ffmpegExec = async ( command: FFmpegCommand, - dataOrPathOrZipItem: Uint8Array | string | ZipItem, + pathOrZipItem: string | ZipItem, outputFileExtension: string, -): Promise => { +): Promise => + withInputFile(pathOrZipItem, async (worker, inputFilePath) => { + const outputFilePath = await makeTempFilePath(outputFileExtension); + try { + await worker.ffmpegExec(command, inputFilePath, outputFilePath); + return await fs.readFile(outputFilePath); + } finally { + await deleteTempFileIgnoringErrors(outputFilePath); + } + }); + +export const withInputFile = async ( + pathOrZipItem: string | ZipItem, + f: (worker: FFmpegUtilityProcess, inputFilePath: string) => Promise, +): Promise => { const worker = await ffmpegUtilityProcess(); const { path: inputFilePath, isFileTemporary: isInputFileTemporary, writeToTemporaryFile: writeToTemporaryInputFile, - } = await makeFileForDataOrStreamOrPathOrZipItem(dataOrPathOrZipItem); + } = await makeFileForStreamOrPathOrZipItem(pathOrZipItem); - const outputFilePath = await makeTempFilePath(outputFileExtension); try { await writeToTemporaryInputFile(); - await worker.ffmpegExec(command, inputFilePath, outputFilePath); - - return await fs.readFile(outputFilePath); + return await f(worker, inputFilePath); } finally { if (isInputFileTemporary) await deleteTempFileIgnoringErrors(inputFilePath); - await deleteTempFileIgnoringErrors(outputFilePath); } }; + +/** + * Implement the IPC "ffmpegDetermineVideoDuration" contract, writing the input + * to temporary files as needed, and then forward to the + * {@link ffmpegDetermineVideoDuration} running in the utility process. + */ +export const ffmpegDetermineVideoDuration = async ( + pathOrZipItem: string | ZipItem, +): Promise => + withInputFile(pathOrZipItem, async (worker, inputFilePath) => + worker.ffmpegDetermineVideoDuration(inputFilePath), + ); diff --git a/desktop/src/main/services/image.ts b/desktop/src/main/services/image.ts index 7daa101d2a..0ac99299eb 100644 --- a/desktop/src/main/services/image.ts +++ b/desktop/src/main/services/image.ts @@ -6,7 +6,7 @@ import { type ZipItem } from "../../types/ipc"; import { execAsync, isDev } from "../utils/electron"; import { deleteTempFileIgnoringErrors, - makeFileForDataOrStreamOrPathOrZipItem, + makeFileForStreamOrPathOrZipItem, makeTempFilePath, } from "../utils/temp"; @@ -61,7 +61,7 @@ const vipsPath = () => ); export const generateImageThumbnail = async ( - dataOrPathOrZipItem: Uint8Array | string | ZipItem, + pathOrZipItem: string | ZipItem, maxDimension: number, maxSize: number, ): Promise => { @@ -69,7 +69,7 @@ export const generateImageThumbnail = async ( path: inputFilePath, isFileTemporary: isInputFileTemporary, writeToTemporaryFile: writeToTemporaryInputFile, - } = await makeFileForDataOrStreamOrPathOrZipItem(dataOrPathOrZipItem); + } = await makeFileForStreamOrPathOrZipItem(pathOrZipItem); const outputFilePath = await makeTempFilePath("jpeg"); diff --git a/desktop/src/main/services/logout.ts b/desktop/src/main/services/logout.ts index 805eb6d109..14968ff24e 100644 --- a/desktop/src/main/services/logout.ts +++ b/desktop/src/main/services/logout.ts @@ -3,6 +3,7 @@ import log from "../log"; import { clearPendingVideoResults } from "../stream"; import { clearStores } from "./store"; import { watchReset } from "./watch"; +import { terminateUtilityProcesses } from "./workers"; import { clearOpenZipCache } from "./zip"; /** @@ -36,4 +37,9 @@ export const logout = (watcher: FSWatcher) => { } catch (e) { ignoreError("zip cache", e); } + try { + terminateUtilityProcesses(); + } catch (e) { + ignoreError("utility processes", e); + } }; diff --git a/desktop/src/main/services/ml-worker.ts b/desktop/src/main/services/ml-worker.ts index fa6ebd1790..234c3abb23 100644 --- a/desktop/src/main/services/ml-worker.ts +++ b/desktop/src/main/services/ml-worker.ts @@ -15,6 +15,7 @@ import { existsSync } from "fs"; import fs from "node:fs/promises"; import path from "node:path"; import * as ort from "onnxruntime-node"; +import { z } from "zod"; import log from "../log-worker"; import { messagePortMainEndpoint } from "../utils/comlink"; import { wait } from "../utils/common"; @@ -23,6 +24,8 @@ import { fsStatMtime } from "./fs"; log.debugString("Started ML utility process"); +process.on("uncaughtException", (e, origin) => log.error(origin, e)); + process.parentPort.once("message", (e) => { // Initialize ourselves with the data we got from our parent. parseInitData(e.data); @@ -50,17 +53,10 @@ let _userDataPath: string | undefined; /** Equivalent to app.getPath("userData") */ const userDataPath = () => _userDataPath!; +const MLWorkerInitData = z.object({ userDataPath: z.string() }); + const parseInitData = (data: unknown) => { - if ( - data && - typeof data == "object" && - "userDataPath" in data && - typeof data.userDataPath == "string" - ) { - _userDataPath = data.userDataPath; - } else { - log.error("Unparseable initialization data"); - } + _userDataPath = MLWorkerInitData.parse(data).userDataPath; }; /** diff --git a/desktop/src/main/services/workers.ts b/desktop/src/main/services/workers.ts index e44b72cd3b..916292ceb6 100644 --- a/desktop/src/main/services/workers.ts +++ b/desktop/src/main/services/workers.ts @@ -15,9 +15,22 @@ import type { UtilityProcessType } from "../../types/ipc"; import log, { processUtilityProcessLogMessage } from "../log"; import { messagePortMainEndpoint } from "../utils/comlink"; +/** + * Terminate any existing utility processes if they're running. + * + * This function is called during the logout sequence. + */ +export const terminateUtilityProcesses = () => { + terminateMLProcessIfRunning(); + terminateFFmpegProcessIfRunning(); +}; + /** The active ML utility process, if any. */ let _utilityProcessML: UtilityProcess | undefined; +/** The active FFmpeg utility process, if any. */ +let _utilityProcessFFmpeg: UtilityProcess | undefined; + /** * A promise to a comlink {@link Endpoint} that can be used to communicate with * the active ffmpeg utility process (if any). @@ -92,18 +105,22 @@ export const triggerCreateUtilityProcess = ( window: BrowserWindow, ) => triggerCreateMLUtilityProcess(window); -export const triggerCreateMLUtilityProcess = (window: BrowserWindow) => { +const terminateMLProcessIfRunning = () => { if (_utilityProcessML) { - log.debug(() => "Terminating previous ML utility process"); + log.debug(() => "Terminating running ML utility process"); _utilityProcessML.kill(); _utilityProcessML = undefined; } +}; + +export const triggerCreateMLUtilityProcess = (window: BrowserWindow) => { + terminateMLProcessIfRunning(); const { port1, port2 } = new MessageChannelMain(); const child = utilityProcess.fork(path.join(__dirname, "ml-worker.js")); const userDataPath = app.getPath("userData"); - child.postMessage({ userDataPath }, [port1]); + child.postMessage(/* MLWorkerInitData */ { userDataPath }, [port1]); window.webContents.postMessage("utilityProcessPort/ml", undefined, [port2]); @@ -173,7 +190,20 @@ const handleMessagesFromMLUtilityProcess = (child: UtilityProcess) => { export const ffmpegUtilityProcessEndpoint = () => (_utilityProcessFFmpegEndpoint ??= createFFmpegUtilityProcessEndpoint()); +const terminateFFmpegProcessIfRunning = () => { + if (_utilityProcessFFmpeg) { + log.debug(() => "Terminating running FFmpeg utility process"); + _utilityProcessFFmpeg.kill(); + _utilityProcessFFmpeg = undefined; + _utilityProcessFFmpegEndpoint = undefined; + } +}; + const createFFmpegUtilityProcessEndpoint = () => { + if (_utilityProcessFFmpeg) { + throw new Error("FFmpeg utility process is already running"); + } + // Promise.withResolvers is currently in the node available to us. let resolve: ((endpoint: Endpoint) => void) | undefined; const promise = new Promise((r) => (resolve = r)); @@ -182,8 +212,10 @@ const createFFmpegUtilityProcessEndpoint = () => { const child = utilityProcess.fork(path.join(__dirname, "ffmpeg-worker.js")); // Send a handle to the port (one end of the message channel) to the utility - // process. The utility process will reply with an "ack" when it get it. - child.postMessage({}, [port1]); + // process (alongwith any other init data). The utility process will reply + // with an "ack" when it get it. + const appVersion = app.getVersion(); + child.postMessage(/* FFmpegWorkerInitData */ { appVersion }, [port1]); child.on("message", (m: unknown) => { if (m && typeof m == "object" && "method" in m) { @@ -201,6 +233,8 @@ const createFFmpegUtilityProcessEndpoint = () => { log.info("Ignoring unknown message from ffmpeg utility process", m); }); + _utilityProcessFFmpeg = child; + // Resolve with the other end of the message channel (once we get an "ack" // from the utility process). return promise; diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index 85d572870f..35339f9a9e 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -14,7 +14,7 @@ import { writeStream } from "./utils/stream"; import { deleteTempFile, deleteTempFileIgnoringErrors, - makeFileForDataOrStreamOrPathOrZipItem, + makeFileForStreamOrPathOrZipItem, makeTempFilePath, } from "./utils/temp"; @@ -277,9 +277,9 @@ const handleVideoDone = async (token: string) => { * * The difference here is that we the conversion generates two streams^ - one * for the HLS playlist itself, and one for the file containing the encrypted - * and transcoded video chunks. The video stream we write to the objectUploadURL - * (provided via {@link params}), and then we return a JSON object containing - * the token for the playlist, and other metadata for use by the renderer. + * and transcoded video chunks. The video stream we write to the pre-signed + * object upload URL(s), and then we return a JSON object containing the token + * for the playlist, and other metadata for use by the renderer. * * ^ if the video doesn't require a stream to be generated (e.g. it is very * small and already uses a compatible codec) then a HTT 204 is returned and @@ -289,10 +289,12 @@ const handleGenerateHLSWrite = async ( request: Request, params: URLSearchParams, ) => { - const objectUploadURL = params.get("objectUploadURL"); - if (!objectUploadURL) throw new Error("Missing objectUploadURL"); + const fileID = parseInt(params.get("fileID") ?? "", 10); + const fetchURL = params.get("fetchURL"); + const authToken = params.get("authToken"); + if (!fileID || !fetchURL || !authToken) throw new Error("Missing params"); - let inputItem: Parameters[0]; + let inputItem: Parameters[0]; const path = params.get("path"); if (path) { inputItem = path; @@ -314,7 +316,7 @@ const handleGenerateHLSWrite = async ( path: inputFilePath, isFileTemporary: isInputFileTemporary, writeToTemporaryFile: writeToTemporaryInputFile, - } = await makeFileForDataOrStreamOrPathOrZipItem(inputItem); + } = await makeFileForStreamOrPathOrZipItem(inputItem); const outputFilePathPrefix = await makeTempFilePath(); let result: FFmpegGenerateHLSPlaylistAndSegmentsResult | undefined; @@ -324,7 +326,9 @@ const handleGenerateHLSWrite = async ( result = await worker.ffmpegGenerateHLSPlaylistAndSegments( inputFilePath, outputFilePathPrefix, - objectUploadURL, + fileID, + fetchURL, + authToken, ); if (!result) { @@ -332,13 +336,18 @@ const handleGenerateHLSWrite = async ( return new Response(null, { status: 204 }); } - const { playlistPath, videoSize, dimensions } = result; + const { playlistPath, dimensions, videoSize, videoObjectID } = result; const playlistToken = randomUUID(); pendingVideoResults.set(playlistToken, playlistPath); return new Response( - JSON.stringify({ playlistToken, videoSize, dimensions }), + JSON.stringify({ + playlistToken, + dimensions, + videoSize, + videoObjectID, + }), { status: 200 }, ); } finally { diff --git a/desktop/src/main/utils/common.ts b/desktop/src/main/utils/common.ts index b123014270..7086e63407 100644 --- a/desktop/src/main/utils/common.ts +++ b/desktop/src/main/utils/common.ts @@ -15,3 +15,11 @@ */ export const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * Convert `null` to `undefined`, passthrough everything else unchanged. + * + * Duplicated from `web/packages/utils/transform.ts`. + */ +export const nullToUndefined = (v: T | null | undefined): T | undefined => + v === null ? undefined : v; diff --git a/desktop/src/main/utils/http.ts b/desktop/src/main/utils/http.ts new file mode 100644 index 0000000000..91a476fed9 --- /dev/null +++ b/desktop/src/main/utils/http.ts @@ -0,0 +1,31 @@ +export const clientPackageName = "io.ente.photos.desktop"; + +/** + * Reimplementation of {@link publicRequestHeaders} from the web source. + * + * @param desktopAppVersion The desktop app's version. This will get passed on + * as the "X-Client-Version" header. + * + * We cannot directly use `app.getVersion()` to obtain this value since the + * {@link app} module is not accessible to Electron utility processes which also + * calls this function. + */ +export const publicRequestHeaders = (desktopAppVersion: string) => ({ + "X-Client-Package": clientPackageName, + "X-Client-Version": desktopAppVersion, +}); + +/** + * Reimplementation of {@link authenticatedRequestHeaders} from the web source. + * + * This builds on top of {@link publicRequestHeaders} and takes the same + * parameters, and additionally also requires the {@link authToken} that will be + * passed as the "X-Auth-Token" header. + */ +export const authenticatedRequestHeaders = ( + desktopAppVersion: string, + authToken: string, +) => ({ + ...publicRequestHeaders(desktopAppVersion), + "X-Auth-Token": authToken, +}); diff --git a/desktop/src/main/utils/temp.ts b/desktop/src/main/utils/temp.ts index 67fd9ae1b9..ad874dd216 100644 --- a/desktop/src/main/utils/temp.ts +++ b/desktop/src/main/utils/temp.ts @@ -80,8 +80,8 @@ export const deleteTempFileIgnoringErrors = async (tempFilePath: string) => { } }; -/** The result of {@link makeFileForDataOrStreamOrPathOrZipItem}. */ -interface FileForDataOrPathOrZipItem { +/** The result of {@link makeFileForStreamOrPathOrZipItem}. */ +interface FileForStreamOrPathOrZipItem { /** * The path to the file (possibly temporary). */ @@ -107,13 +107,13 @@ interface FileForDataOrPathOrZipItem { * that needs to be deleted after processing, and a function to write the given * {@link item} into that temporary file if needed. * - * @param item The contents of the file (bytes), or a {@link ReadableStream} - * with the contents of the file, or the path to an existing file, or a (path to - * a zip file, name of an entry within that zip file) tuple. + * @param item A {@link ReadableStream} with the contents of the file, or the + * path to an existing file, or a (path to a zip file, name of an entry within + * that zip file) tuple. */ -export const makeFileForDataOrStreamOrPathOrZipItem = async ( - item: Uint8Array | ReadableStream | string | ZipItem, -): Promise => { +export const makeFileForStreamOrPathOrZipItem = async ( + item: ReadableStream | string | ZipItem, +): Promise => { let path: string; let isFileTemporary: boolean; let writeToTemporaryFile = async () => { @@ -126,9 +126,7 @@ export const makeFileForDataOrStreamOrPathOrZipItem = async ( } else { path = await makeTempFilePath(); isFileTemporary = true; - if (item instanceof Uint8Array) { - writeToTemporaryFile = () => fs.writeFile(path, item); - } else if (item instanceof ReadableStream) { + if (item instanceof ReadableStream) { writeToTemporaryFile = () => writeStream(path, item); } else { writeToTemporaryFile = async () => { diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 20ace5ab8c..e983855d5f 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -193,29 +193,32 @@ const convertToJPEG = (imageData: Uint8Array) => ipcRenderer.invoke("convertToJPEG", imageData); const generateImageThumbnail = ( - dataOrPathOrZipItem: Uint8Array | string | ZipItem, + pathOrZipItem: string | ZipItem, maxDimension: number, maxSize: number, ) => ipcRenderer.invoke( "generateImageThumbnail", - dataOrPathOrZipItem, + pathOrZipItem, maxDimension, maxSize, ); const ffmpegExec = ( command: FFmpegCommand, - dataOrPathOrZipItem: Uint8Array | string | ZipItem, + pathOrZipItem: string | ZipItem, outputFileExtension: string, ) => ipcRenderer.invoke( "ffmpegExec", command, - dataOrPathOrZipItem, + pathOrZipItem, outputFileExtension, ); +const ffmpegDetermineVideoDuration = (pathOrZipItem: string | ZipItem) => + ipcRenderer.invoke("ffmpegDetermineVideoDuration", pathOrZipItem); + // - Utility processes const triggerCreateUtilityProcess = (type: UtilityProcessType) => { @@ -392,6 +395,7 @@ contextBridge.exposeInMainWorld("electron", { convertToJPEG, generateImageThumbnail, ffmpegExec, + ffmpegDetermineVideoDuration, // - ML diff --git a/desktop/yarn.lock b/desktop/yarn.lock index 25b3a8f43a..00bd49e933 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -136,13 +136,20 @@ minimatch "^9.0.3" plist "^3.1.0" -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": +"@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== dependencies: eslint-visitor-keys "^3.3.0" +"@eslint-community/eslint-utils@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a" + integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw== + dependencies: + eslint-visitor-keys "^3.4.3" + "@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.11.0": version "4.11.0" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae" @@ -177,10 +184,10 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.9.1.tgz#4a97e85e982099d6c7ee8410aacb55adaa576f06" integrity sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ== -"@eslint/js@^9.25.1": - version "9.25.1" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.25.1.tgz#25f5c930c2b68b5ebe7ac857f754cbd61ef6d117" - integrity sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg== +"@eslint/js@^9.27.0": + version "9.27.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.27.0.tgz#181a23460877c484f6dd03890f4e3fa2fdeb8ff0" + integrity sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA== "@eslint/object-schema@^2.1.4": version "2.1.4" @@ -268,10 +275,10 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@pkgr/core@^0.1.0": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" - integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== +"@pkgr/core@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.4.tgz#d897170a2b0ba51f78a099edccd968f7b103387c" + integrity sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw== "@sindresorhus/is@^4.0.0": version "4.6.0" @@ -290,10 +297,10 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== -"@tsconfig/node22@^22.0.1": - version "22.0.1" - resolved "https://registry.yarnpkg.com/@tsconfig/node22/-/node22-22.0.1.tgz#27e3ee9b359e31e5b94690bf2bad5a923c1d57d0" - integrity sha512-VkgOa3n6jvs1p+r3DiwBqeEwGAwEvnVCg/hIjiANl5IEcqP3G0u5m8cBJspe1t9qjZRlZ7WFgqq5bJrGdgAKMg== +"@tsconfig/node22@^22.0.2": + version "22.0.2" + resolved "https://registry.yarnpkg.com/@tsconfig/node22/-/node22-22.0.2.tgz#1e04e2c5cc946dac787d69bb502462a851ae51b6" + integrity sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA== "@types/auto-launch@^5.0.5": version "5.0.5" @@ -385,85 +392,85 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz#62f1befe59647524994e89de4516d8dcba7a850a" - integrity sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ== +"@typescript-eslint/eslint-plugin@8.32.1": + version "8.32.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz#9185b3eaa3b083d8318910e12d56c68b3c4f45b4" + integrity sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.31.1" - "@typescript-eslint/type-utils" "8.31.1" - "@typescript-eslint/utils" "8.31.1" - "@typescript-eslint/visitor-keys" "8.31.1" + "@typescript-eslint/scope-manager" "8.32.1" + "@typescript-eslint/type-utils" "8.32.1" + "@typescript-eslint/utils" "8.32.1" + "@typescript-eslint/visitor-keys" "8.32.1" graphemer "^1.4.0" - ignore "^5.3.1" + ignore "^7.0.0" natural-compare "^1.4.0" - ts-api-utils "^2.0.1" + ts-api-utils "^2.1.0" -"@typescript-eslint/parser@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.31.1.tgz#e9b0ccf30d37dde724ee4d15f4dbc195995cce1b" - integrity sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q== +"@typescript-eslint/parser@8.32.1": + version "8.32.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.32.1.tgz#18b0e53315e0bc22b2619d398ae49a968370935e" + integrity sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg== dependencies: - "@typescript-eslint/scope-manager" "8.31.1" - "@typescript-eslint/types" "8.31.1" - "@typescript-eslint/typescript-estree" "8.31.1" - "@typescript-eslint/visitor-keys" "8.31.1" + "@typescript-eslint/scope-manager" "8.32.1" + "@typescript-eslint/types" "8.32.1" + "@typescript-eslint/typescript-estree" "8.32.1" + "@typescript-eslint/visitor-keys" "8.32.1" debug "^4.3.4" -"@typescript-eslint/scope-manager@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz#1eb52e76878f545e4add142e0d8e3e97e7aa443b" - integrity sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw== +"@typescript-eslint/scope-manager@8.32.1": + version "8.32.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz#9a6bf5fb2c5380e14fe9d38ccac6e4bbe17e8afc" + integrity sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA== dependencies: - "@typescript-eslint/types" "8.31.1" - "@typescript-eslint/visitor-keys" "8.31.1" + "@typescript-eslint/types" "8.32.1" + "@typescript-eslint/visitor-keys" "8.32.1" -"@typescript-eslint/type-utils@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz#be0f438fb24b03568e282a0aed85f776409f970c" - integrity sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA== +"@typescript-eslint/type-utils@8.32.1": + version "8.32.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz#b9292a45f69ecdb7db74d1696e57d1a89514d21e" + integrity sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA== dependencies: - "@typescript-eslint/typescript-estree" "8.31.1" - "@typescript-eslint/utils" "8.31.1" + "@typescript-eslint/typescript-estree" "8.32.1" + "@typescript-eslint/utils" "8.32.1" debug "^4.3.4" - ts-api-utils "^2.0.1" + ts-api-utils "^2.1.0" -"@typescript-eslint/types@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.31.1.tgz#478ed6f7e8aee1be7b63a60212b6bffe1423b5d4" - integrity sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ== +"@typescript-eslint/types@8.32.1": + version "8.32.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.32.1.tgz#b19fe4ac0dc08317bae0ce9ec1168123576c1d4b" + integrity sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg== -"@typescript-eslint/typescript-estree@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz#37792fe7ef4d3021c7580067c8f1ae66daabacdf" - integrity sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag== +"@typescript-eslint/typescript-estree@8.32.1": + version "8.32.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz#9023720ca4ecf4f59c275a05b5fed69b1276face" + integrity sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg== dependencies: - "@typescript-eslint/types" "8.31.1" - "@typescript-eslint/visitor-keys" "8.31.1" + "@typescript-eslint/types" "8.32.1" + "@typescript-eslint/visitor-keys" "8.32.1" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" minimatch "^9.0.4" semver "^7.6.0" - ts-api-utils "^2.0.1" + ts-api-utils "^2.1.0" -"@typescript-eslint/utils@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.31.1.tgz#5628ea0393598a0b2f143d0fc6d019f0dee9dd14" - integrity sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ== +"@typescript-eslint/utils@8.32.1": + version "8.32.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.32.1.tgz#4d6d5d29b9e519e9a85e9a74e9f7bdb58abe9704" + integrity sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA== dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.31.1" - "@typescript-eslint/types" "8.31.1" - "@typescript-eslint/typescript-estree" "8.31.1" + "@eslint-community/eslint-utils" "^4.7.0" + "@typescript-eslint/scope-manager" "8.32.1" + "@typescript-eslint/types" "8.32.1" + "@typescript-eslint/typescript-estree" "8.32.1" -"@typescript-eslint/visitor-keys@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz#6742b0e3ba1e0c1e35bdaf78c03e759eb8dd8e75" - integrity sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw== +"@typescript-eslint/visitor-keys@8.32.1": + version "8.32.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz#4321395cc55c2eb46036cbbb03e101994d11ddca" + integrity sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w== dependencies: - "@typescript-eslint/types" "8.31.1" + "@typescript-eslint/types" "8.32.1" eslint-visitor-keys "^4.2.0" "@xmldom/xmldom@^0.8.8": @@ -1007,6 +1014,17 @@ cross-env@^7.0.3: dependencies: cross-spawn "^7.0.1" +cross-spawn@^6.0.0: + version "6.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.6.tgz#30d0efa0712ddb7eb5a76e1e8721bffafa6b5d57" + integrity sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" @@ -1087,7 +1105,7 @@ detect-libc@^2.0.1: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== -detect-newline@^4.0.0: +detect-newline@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-4.0.1.tgz#fcefdb5713e1fb8cb2839b8b6ee22e6716ab8f23" integrity sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog== @@ -1216,10 +1234,10 @@ electron-updater@^6.6.3: semver "^7.6.3" tiny-typed-emitter "^2.1.0" -electron@^36.1.0: - version "36.1.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-36.1.0.tgz#9919b77e61cd1400acc6dd24f9db8451fba5f8eb" - integrity sha512-gnp3BnbKdGsVc7cm1qlEaZc8pJsR08mIs8H/yTo8gHEtFkGGJbDTVZOYNAfbQlL0aXh+ozv+CnyiNeDNkT1Upg== +electron@^36.3.2: + version "36.3.2" + resolved "https://registry.yarnpkg.com/electron/-/electron-36.3.2.tgz#4a60f95e8d3858d01570c03b58dc2fb2f17ee8b6" + integrity sha512-v0/j7n22CL3OYv9BIhq6JJz2+e1HmY9H4bjTk8/WzVT9JwVX/T/21YNdR7xuQ6XDSEo9gP5JnqmjOamE+CUY8Q== dependencies: "@electron/get" "^2.0.0" "@types/node" "^22.7.7" @@ -1289,7 +1307,7 @@ eslint-scope@^8.0.2: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.3.0: +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== @@ -1372,6 +1390,19 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + exponential-backoff@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" @@ -1438,10 +1469,10 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -fdir@^6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689" - integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ== +fdir@^6.4.4: + version "6.4.4" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9" + integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg== ffmpeg-static@^5.2.0: version "5.2.0" @@ -1589,10 +1620,12 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: has-symbols "^1.0.3" hasown "^2.0.0" -get-stdin@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-9.0.0.tgz#3983ff82e03d56f1b2ea0d3e60325f39d703a575" - integrity sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA== +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" get-stream@^5.1.0: version "5.2.0" @@ -1601,10 +1634,10 @@ get-stream@^5.1.0: dependencies: pump "^3.0.0" -git-hooks-list@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-3.1.0.tgz#386dc531dcc17474cf094743ff30987a3d3e70fc" - integrity sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA== +git-hooks-list@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-4.1.1.tgz#ae340b82a9312354c73b48007f33840bbd83d3c0" + integrity sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA== glob-parent@^5.1.2: version "5.1.2" @@ -1632,7 +1665,7 @@ glob@^10.3.12, glob@^10.3.7: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: +glob@^7.1.3, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -1830,11 +1863,16 @@ ieee754@^1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.2.0, ignore@^5.3.1: +ignore@^5.2.0: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== +ignore@^7.0.0: + version "7.0.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.4.tgz#a12c70d0f2607c5bf508fb65a40c75f037d7a078" + integrity sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A== + import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -1945,6 +1983,11 @@ is-plain-obj@^4.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== + is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" @@ -2244,7 +2287,7 @@ minimatch@^9.0.3, minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -2363,6 +2406,11 @@ next-electron-server@^1.0.0: resolved "https://registry.yarnpkg.com/next-electron-server/-/next-electron-server-1.0.0.tgz#03e133ed64a5ef671b6c6409f908c4901b1828cb" integrity sha512-fTUaHwT0Jry2fbdUSIkAiIqgDAInI5BJFF4/j90/okvZCYlyx6yxpXB30KpzmOG6TN/ESwyvsFJVvS2WHT8PAA== +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + node-abi@^3.45.0: version "3.67.0" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.67.0.tgz#1d159907f18d18e18809dbbb5df47ed2426a08df" @@ -2399,6 +2447,13 @@ normalize-url@^6.0.1: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw== + dependencies: + path-key "^2.0.0" + object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -2463,6 +2518,11 @@ p-cancelable@^2.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== + p-limit@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -2535,6 +2595,11 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== + path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -2599,13 +2664,13 @@ prettier-plugin-organize-imports@^4.1.0: resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz#f3d3764046a8e7ba6491431158b9be6ffd83b90f" integrity sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A== -prettier-plugin-packagejson@^2.5.10: - version "2.5.10" - resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.10.tgz#f47068d0aa12efcdddb802189d8adae874ba00e7" - integrity sha512-LUxATI5YsImIVSaaLJlJ3aE6wTD+nvots18U3GuQMJpUyClChaZlQrqx3dBnbhF20OnKWZyx8EgyZypQtBDtgQ== +prettier-plugin-packagejson@^2.5.14: + version "2.5.14" + resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.14.tgz#8ada09114ff60c7f42c3f8755ffb2f8152f3624f" + integrity sha512-h+3tSpr2nVpp+YOK1MDIYtYhHVXr8/0V59UUbJpIJFaqi3w4fvUokJo6eV8W+vELrUXIZzJ+DKm5G7lYzrMcKQ== dependencies: - sort-package-json "2.15.1" - synckit "0.9.2" + sort-package-json "3.2.1" + synckit "0.11.6" prettier@3.5.3: version "3.5.3" @@ -2829,6 +2894,11 @@ semver@^7.3.2, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.6.0, semve resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +semver@^7.7.1: + version "7.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + serialize-error@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18" @@ -2836,6 +2906,13 @@ serialize-error@^7.0.1: dependencies: type-fest "^0.13.1" +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== + dependencies: + shebang-regex "^1.0.0" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -2843,6 +2920,11 @@ shebang-command@^2.0.0: dependencies: shebang-regex "^3.0.0" +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== + shebang-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" @@ -2853,24 +2935,25 @@ shell-quote@^1.8.1: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== -shelljs@^0.8.5: - version "0.8.5" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" - integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== +shelljs@^0.9.2: + version "0.9.2" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.9.2.tgz#a8ac724434520cd7ae24d52071e37a18ac2bb183" + integrity sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw== dependencies: - glob "^7.0.0" + execa "^1.0.0" + fast-glob "^3.3.2" interpret "^1.0.0" rechoir "^0.6.2" -shx@^0.3.4: - version "0.3.4" - resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.4.tgz#74289230b4b663979167f94e1935901406e40f02" - integrity sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g== +shx@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/shx/-/shx-0.4.0.tgz#c6ea6ace7e778da0ab32d2eab9def59d788e9336" + integrity sha512-Z0KixSIlGPpijKgcH6oCMCbltPImvaKy0sGH8AkLRXw1KyzpKtaCTizP2xen+hNDqVF4xxgvA0KXSb9o4Q6hnA== dependencies: - minimist "^1.2.3" - shelljs "^0.8.5" + minimist "^1.2.8" + shelljs "^0.9.2" -signal-exit@^3.0.2: +signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -2923,19 +3006,18 @@ sort-object-keys@^1.1.3: resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45" integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg== -sort-package-json@2.15.1: - version "2.15.1" - resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.15.1.tgz#e5a035fad7da277b1947b9eecc93ea09c1c2526e" - integrity sha512-9x9+o8krTT2saA9liI4BljNjwAbvUnWf11Wq+i/iZt8nl2UGYnf3TH5uBydE7VALmP7AGwlfszuEeL8BDyb0YA== +sort-package-json@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-3.2.1.tgz#889f3bdf43ceeff5fa4278a7c53ae5b1520d287e" + integrity sha512-rTfRdb20vuoAn7LDlEtCqOkYfl2X+Qze6cLbNOzcDpbmKEhJI30tTN44d5shbKJnXsvz24QQhlCm81Bag7EOKg== dependencies: detect-indent "^7.0.1" - detect-newline "^4.0.0" - get-stdin "^9.0.0" - git-hooks-list "^3.0.0" + detect-newline "^4.0.1" + git-hooks-list "^4.0.0" is-plain-obj "^4.1.0" - semver "^7.6.0" + semver "^7.7.1" sort-object-keys "^1.1.3" - tinyglobby "^0.2.9" + tinyglobby "^0.2.12" source-map-support@^0.5.19: version "0.5.21" @@ -2990,6 +3072,11 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q== + strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" @@ -3021,13 +3108,12 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -synckit@0.9.2: - version "0.9.2" - resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.2.tgz#a3a935eca7922d48b9e7d6c61822ee6c3ae4ec62" - integrity sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw== +synckit@0.11.6: + version "0.11.6" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.6.tgz#e742a0c27bbc1fbc96f2010770521015cca7ed5c" + integrity sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw== dependencies: - "@pkgr/core" "^0.1.0" - tslib "^2.6.2" + "@pkgr/core" "^0.2.4" tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.2.1: version "6.2.1" @@ -3078,12 +3164,12 @@ tiny-typed-emitter@^2.1.0: resolved "https://registry.yarnpkg.com/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz#b3b027fdd389ff81a152c8e847ee2f5be9fad7b5" integrity sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA== -tinyglobby@^0.2.9: - version "0.2.10" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.10.tgz#e712cf2dc9b95a1f5c5bbd159720e15833977a0f" - integrity sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew== +tinyglobby@^0.2.12: + version "0.2.13" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.13.tgz#a0e46515ce6cbcd65331537e57484af5a7b2ff7e" + integrity sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw== dependencies: - fdir "^6.4.2" + fdir "^6.4.4" picomatch "^4.0.2" tmp-promise@^3.0.2: @@ -3117,12 +3203,12 @@ truncate-utf8-bytes@^1.0.0: dependencies: utf8-byte-length "^1.0.1" -ts-api-utils@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.0.1.tgz#660729385b625b939aaa58054f45c058f33f10cd" - integrity sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w== +ts-api-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" + integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== -tslib@^2.1.0, tslib@^2.6.2: +tslib@^2.1.0: version "2.7.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== @@ -3149,14 +3235,14 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript-eslint@^8.31.1: - version "8.31.1" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.31.1.tgz#b77ab1e48ced2daab9225ff94bab54391a4af69b" - integrity sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA== +typescript-eslint@^8.32.1: + version "8.32.1" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.32.1.tgz#1784335c781491be528ff84ab666e2f0f7591fd1" + integrity sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg== dependencies: - "@typescript-eslint/eslint-plugin" "8.31.1" - "@typescript-eslint/parser" "8.31.1" - "@typescript-eslint/utils" "8.31.1" + "@typescript-eslint/eslint-plugin" "8.32.1" + "@typescript-eslint/parser" "8.32.1" + "@typescript-eslint/utils" "8.32.1" typescript@^5.4.3, typescript@^5.8.3: version "5.8.3" @@ -3230,6 +3316,13 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -3311,3 +3404,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@^3.25.23: + version "3.25.23" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.23.tgz#128fb02f3619a8bca6bbbf6b07b457236cf33391" + integrity sha512-Od2bdMosahjSrSgJtakrwjMDb1zM1A3VIHCPGveZt/3/wlrTWBya2lmEh2OYe4OIu8mPTmmr0gnLHIWQXdtWBg== diff --git a/docs/docs/auth/faq/index.md b/docs/docs/auth/faq/index.md index 5e631906aa..b731fa573e 100644 --- a/docs/docs/auth/faq/index.md +++ b/docs/docs/auth/faq/index.md @@ -41,6 +41,16 @@ Usually, this discrepancy occurs because the time in your browser might be incorrect. In particular, multiple users have reported that Firefox provides incorrect time when certain privacy settings are enabled. +> [!TIP] +> +> Newer Ente Auth clients (upcoming 4.4.0+) will automatically try to correct +> for incorrect system time, so you should be seeing correct codes even if your +> system time is out of sync. However, this automatic correction will not work +> if you're using Ente Auth in offline mode. +> +> If you've recently changed your system time and the codes are still incorrect, +> try to refresh / restart the app if needed. + ### Can I access my codes on web? You can access your codes on the web at [auth.ente.io](https://auth.ente.io). diff --git a/docs/docs/photos/faq/export.md b/docs/docs/photos/faq/export.md index 16879ee327..e6ed64b4fa 100644 --- a/docs/docs/photos/faq/export.md +++ b/docs/docs/photos/faq/export.md @@ -21,7 +21,7 @@ always available on your machine. background without you needing to run any other cron jobs. See [migration/export](/photos/migration/export/) for more details. -## Does the exported data preserve folder structure? +## Does the exported data preserve album structure? Yes. When you export your data for local backup, it will maintain the exact album structure how you have set up within Ente. diff --git a/docs/docs/photos/faq/video-streaming.md b/docs/docs/photos/faq/video-streaming.md index 59a34907f6..3d07223a14 100644 --- a/docs/docs/photos/faq/video-streaming.md +++ b/docs/docs/photos/faq/video-streaming.md @@ -8,22 +8,43 @@ description: > [!NOTE] > -> Video streaming is available in beta on mobile apps starting v0.9.98. +> Video streaming is available in beta on mobile apps starting v0.9.98 and on +> desktop starting v1.7.13. ### How to enable video streaming? +#### On mobile + - Open Settings -> General -> Advanced -- Switch on the toggle for `Video streaming` +- Enable the toggle for `Streamable videos` + +#### On desktop + +- Open Settings -> Preferences +- Enable the toggle for `Streamable videos` ### What happens when I enable video streaming? +#### On mobile + Enabling video streaming will start processing videos captured in the last 30 days, generating streams for each. Both local and remote videos will be processed, so this may consume bandwidth for downloading of remote files and uploading of the generated streams. +#### On desktop + +When enabled, the desktop app will generate streams both for new uploads, and +also for all existing videos that were previously uploaded. + +Stream generation is CPU intensive and can take time so the app will continue +processing them in the background. Clicking on search bar will show "Processing +videos..." when stream generation is happening. + ### How can I view video streams? +### On mobile + Settings -> Backup > Backup status will show details regarding the processing status for videos. Processed videos will have a green play button next to them. You can open these videos by tapping on them. @@ -34,6 +55,12 @@ play the stream. Clicking on the `Info` icon within the original video will show details about the generated stream. +### On desktop and web + +Desktop and web app will automatically play the streaming version of a video if +it has been already generated. The quality selector will show "Auto" when +playing the stream. + ### What is a stream? Stream is an encrypted HLS file with an `.m3u8` playlist that helps play a video diff --git a/docs/docs/self-hosting/guides/admin.md b/docs/docs/self-hosting/guides/admin.md index 75048f2804..3f80d1478e 100644 --- a/docs/docs/self-hosting/guides/admin.md +++ b/docs/docs/self-hosting/guides/admin.md @@ -3,32 +3,6 @@ title: Server admin description: Administering your custom self-hosted Ente instance using the CLI --- -# Administering your custom server - -You can use -[Ente's CLI](https://github.com/ente-io/ente/releases?q=tag%3Acli-v0) to -administer your self hosted server. - -First we need to get your CLI to connect to your custom server. Define a -config.yaml and put it either in the same directory as CLI or path defined in -env variable `ENTE_CLI_CONFIG_PATH` - -```yaml -endpoint: - api: "http://localhost:8080" -``` - -Now you should be able to -[add an account](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_account_add.md), -and subsequently increase the -[storage and account validity](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_admin_update-subscription.md) -using the CLI. - -> [!NOTE] -> -> The CLI command to add an account does not create Ente accounts. It only adds -> existing accounts to the list of (existing) accounts that the CLI can use. - ## Becoming an admin By default, the first user (and only the first user) created on the system is @@ -40,7 +14,7 @@ explicit whitelist of admins. > [!NOTE] > -> The first user is only treated as the admin if the list of admins in the +> The first user is only treated as the admin if the list of admins in the > configuration is empty. > > Also, if at some point you delete the first user, then you will need to define @@ -78,6 +52,38 @@ You can use [account list](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_account_list.md) command to find the user id of any account. +# Administering your custom server + +> [!NOTE] +> For the first user (admin) to perform administrative actions using the CLI, their +> userID must be whitelisted in the `museum.yaml` configuration file under +> `internal.admins`. While the first user is automatically granted admin privileges +> on the server, this additional step is required for CLI operations. + +You can use +[Ente's CLI](https://github.com/ente-io/ente/releases?q=tag%3Acli-v0) to +administer your self hosted server. + +First we need to get your CLI to connect to your custom server. Define a +config.yaml and put it either in the same directory as CLI or path defined in +env variable `ENTE_CLI_CONFIG_PATH` + +```yaml +endpoint: + api: "http://localhost:8080" +``` + +Now you should be able to +[add an account](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_account_add.md), +and subsequently increase the +[storage and account validity](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_admin_update-subscription.md) +using the CLI. + +> [!NOTE] +> +> The CLI command to add an account does not create Ente accounts. It only adds +> existing accounts to the list of (existing) accounts that the CLI can use. + ## Backups See this [FAQ](/self-hosting/faq/backup). diff --git a/docs/docs/self-hosting/museum.md b/docs/docs/self-hosting/museum.md index f1ef92fa51..515616ac32 100644 --- a/docs/docs/self-hosting/museum.md +++ b/docs/docs/self-hosting/museum.md @@ -49,10 +49,10 @@ For example, ```yaml apps: - public-albums: albums.myente.xyz - cast: cast.myente.xyz - accounts: accounts.myente.xyz - family: family.myente.xyz + public-albums: https://albums.myente.xyz + cast: https://cast.myente.xyz + accounts: https://accounts.myente.xyz + family: https://family.myente.xyz ``` >[!IMPORTANT] @@ -74,4 +74,4 @@ functionalities like SMTP, Discord notifications, Hardcoded-OTTs, etc. ## References -- [Environment variables and ports](/self-hosting/faq/environment) \ No newline at end of file +- [Environment variables and ports](/self-hosting/faq/environment) diff --git a/docs/docs/self-hosting/troubleshooting/uploads.md b/docs/docs/self-hosting/troubleshooting/uploads.md index 6bfef9f923..3dbc7454f4 100644 --- a/docs/docs/self-hosting/troubleshooting/uploads.md +++ b/docs/docs/self-hosting/troubleshooting/uploads.md @@ -3,14 +3,14 @@ title: Uploads description: Fixing upload errors when trying to self host Ente --- -# Troubleshooting upload failures +# Troubleshooting upload failures Here are some errors our community members frequently encountered with the context and potential fixes. Fundamentally in most situations, the problem is because of minor mistakes or -misconfiguration. Please make sure to reverse proxy museum and MinIO API -endpoint to a domain and check your S3 credentials and whole configuration +misconfiguration. Please make sure to reverse proxy museum and MinIO API +endpoint to a domain and check your S3 credentials and whole configuration file for any minor misconfigurations. It is also suggested that the user setups bucket CORS on MinIO or any external @@ -21,10 +21,10 @@ this](/self-hosting/troubleshooting/bucket-cors). S3 is an cloud storage protocol made by Amazon (specifically AWS). S3 is designed to store files and data as objects inside Buckets and it is mostly used for Online -Backups and storing different types of files. +Backups and storing different types of files. -Ente's Docker setup is shipped with [MinIO](https://min.io/) as its default S3 provider. -MinIO supports the Amazon S3 protocol and leverages your disk storage to +Ente's Docker setup is shipped with [MinIO](https://min.io/) as its default S3 provider. +MinIO supports the Amazon S3 protocol and leverages your disk storage to dump all the uploaded files as encrypted object blobs. ## 403 Forbidden @@ -40,15 +40,15 @@ This could be because 1. The bucket CORS rules do not allow museum to access these objects. For uploading files from the browser, you will need to set `allowedOrigins` to - `*`, and allow the `X-Auth-Token`, `X-Client-Package` headers configuration - too. [Here is an example of a working + `*`, and allow the `X-Auth-Token`, `X-Client-Package`, `X-Client-Version` + headers configuration too. [Here is an example of a working configuration](https://github.com/ente-io/ente/discussions/1764#discussioncomment-9478204). 2. The credentials are not being picked up (you might be setting the correct credentials, but not in the place where museum reads them from). -## Mismatch in file size +## Mismatch in file size The "Mismatch in file size" error mostly occurs in a situation where the client is re-uploading a file which is already in the bucket with a different file size. The reason for re-upload could be anything including network issue, -sudden killing of app before the upload is complete and etc. +sudden killing of app before the upload is complete and etc. diff --git a/infra/copycat-db/src/backup.sh b/infra/copycat-db/src/backup.sh index f197f4b0f0..bbd8a2bb9e 100755 --- a/infra/copycat-db/src/backup.sh +++ b/infra/copycat-db/src/backup.sh @@ -36,7 +36,7 @@ else BACKUP_ID=$(scw rdb backup create instance-id=$SCW_RDB_INSTANCE_ID \ name=$BACKUP_NAME expires-at=$EXPIRY \ database-name=ente_db -o json | jq -r '.id') - scw rdb backup wait $BACKUP_ID timeout=5h + scw rdb backup wait $BACKUP_ID timeout=8h scw rdb backup download output=$BACKUP_FILE \ $(scw rdb backup export $BACKUP_ID --wait -o json | jq -r '.id') fi diff --git a/infra/workers/csp-reporter/package.json b/infra/workers/csp-reporter/package.json new file mode 100644 index 0000000000..80c9a9d516 --- /dev/null +++ b/infra/workers/csp-reporter/package.json @@ -0,0 +1,5 @@ +{ + "name": "csp-reporter", + "version": "0.0.0", + "private": true +} diff --git a/infra/workers/csp-reporter/src/index.ts b/infra/workers/csp-reporter/src/index.ts new file mode 100644 index 0000000000..34af4cd68e --- /dev/null +++ b/infra/workers/csp-reporter/src/index.ts @@ -0,0 +1,23 @@ +/** + * Log CSP reports. + * + * See _headers in the web app source. + */ + +export default { + async fetch(request: Request) { + switch (request.method) { + case "POST": + return handlePOST(request); + default: + console.log(`Unsupported HTTP method ${request.method}`); + return new Response(null, { status: 405 }); + } + }, +} satisfies ExportedHandler; + +const handlePOST = async (request: Request) => { + // {job="worker"} |= `[csp-report]` | json log="logs[0]" | keep log + console.log("[csp-report]", await request.text()); + return new Response(null, { status: 200 }); +}; diff --git a/infra/workers/csp-reporter/tsconfig.json b/infra/workers/csp-reporter/tsconfig.json new file mode 100644 index 0000000000..a65b752070 --- /dev/null +++ b/infra/workers/csp-reporter/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../tsconfig.base.json", "include": ["src"] } diff --git a/infra/workers/csp-reporter/wrangler.toml b/infra/workers/csp-reporter/wrangler.toml new file mode 100644 index 0000000000..8cdc2f9682 --- /dev/null +++ b/infra/workers/csp-reporter/wrangler.toml @@ -0,0 +1,9 @@ +name = "csp-reporter" +main = "src/index.ts" +compatibility_date = "2025-05-19" + +routes = [ + { pattern = "csp-reporter.ente.io", custom_domain = true } +] + +tail_consumers = [{ service = "tail" }] diff --git a/infra/workers/files/src/index.ts b/infra/workers/files/src/index.ts index e855dca243..5b9452a450 100644 --- a/infra/workers/files/src/index.ts +++ b/infra/workers/files/src/index.ts @@ -21,7 +21,7 @@ const handleOPTIONS = (request: Request) => { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", - "Access-Control-Allow-Headers": "X-Auth-Token, X-Client-Package", + "Access-Control-Allow-Headers": "X-Auth-Token, X-Client-Package, X-Client-Version", "Access-Control-Max-Age": "86400", }, }); diff --git a/infra/workers/package.json b/infra/workers/package.json index d9a555362f..e9e27cde23 100644 --- a/infra/workers/package.json +++ b/infra/workers/package.json @@ -2,10 +2,10 @@ "name": "workers", "private": true, "devDependencies": { - "@cloudflare/workers-types": "^4.20240614.0", - "typescript": "^5", - "wrangler": "^3", - "prettier": "^3.3.4" + "@cloudflare/workers-types": "^4.20250519.0", + "typescript": "^5.8.3", + "wrangler": "^4.15.2", + "prettier": "^3.5.3" }, "workspaces": [ "*" diff --git a/infra/workers/public-albums/src/index.ts b/infra/workers/public-albums/src/index.ts index 505a474635..48fed6e38f 100644 --- a/infra/workers/public-albums/src/index.ts +++ b/infra/workers/public-albums/src/index.ts @@ -22,7 +22,7 @@ const handleOPTIONS = (request: Request) => { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": - "X-Auth-Access-Token, X-Auth-Access-Token-JWT, X-Client-Package", + "X-Auth-Access-Token, X-Auth-Access-Token-JWT, X-Client-Package, X-Client-Version", "Access-Control-Max-Age": "86400", }, }); diff --git a/infra/workers/thumbnails/src/index.ts b/infra/workers/thumbnails/src/index.ts index 9fc23fa52b..2108e50258 100644 --- a/infra/workers/thumbnails/src/index.ts +++ b/infra/workers/thumbnails/src/index.ts @@ -21,7 +21,7 @@ const handleOPTIONS = (request: Request) => { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", - "Access-Control-Allow-Headers": "X-Auth-Token, X-Client-Package", + "Access-Control-Allow-Headers": "X-Auth-Token, X-Client-Package, X-Client-Version", "Access-Control-Max-Age": "86400", }, }); diff --git a/infra/workers/uploader/src/index.ts b/infra/workers/uploader/src/index.ts index fb811924be..62396e5c8d 100644 --- a/infra/workers/uploader/src/index.ts +++ b/infra/workers/uploader/src/index.ts @@ -28,7 +28,7 @@ const handleOPTIONS = (request: Request) => { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, PUT, OPTIONS", "Access-Control-Allow-Headers": - "Content-Type, UPLOAD-URL, X-Client-Package", + "Content-Type, UPLOAD-URL, X-Client-Package, X-Client-Version", "Access-Control-Expose-Headers": "X-Request-Id, CF-Ray", "Access-Control-Max-Age": "86400", }, diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index a5fe360712..2d1fb835f0 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -53,7 +53,7 @@ analyzer: sort_child_properties_last: warning sort_pub_dependencies: warning library_private_types_in_public_api: warning - constant_identifier_names: warning + constant_identifier_names: ignore prefer_const_constructors: warning prefer_const_declarations: warning prefer_const_constructors_in_immutables: warning diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 60f760e2c4..4583051e80 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -8,7 +8,10 @@ android:requestLegacyExternalStorage="true" android:allowBackup="false" android:fullBackupContent="false" - android:largeHeap="true"> + android:dataExtractionRules="@xml/data_extraction_rules" + android:largeHeap="true" + android:networkSecurityConfig="@xml/network_security_config" + > + + + + + + + + + + + + + + + + + + + + + @@ -177,4 +203,6 @@ + + \ No newline at end of file diff --git a/mobile/android/app/src/main/kotlin/io/ente/photos/EnteAlbumsWidgetProvider.kt b/mobile/android/app/src/main/kotlin/io/ente/photos/EnteAlbumsWidgetProvider.kt new file mode 100644 index 0000000000..147d5ddea6 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/io/ente/photos/EnteAlbumsWidgetProvider.kt @@ -0,0 +1,195 @@ +package io.ente.photos + +import android.appwidget.AppWidgetManager +import android.content.Context +import android.content.SharedPreferences +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable +import android.net.Uri +import android.util.Log +import android.view.View +import android.widget.RemoteViews +import androidx.core.content.ContextCompat +import es.antonborri.home_widget.HomeWidgetLaunchIntent +import es.antonborri.home_widget.HomeWidgetProvider +import java.io.File +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json + +@Serializable +data class AlbumsFileData( + val title: String?, + val subText: String?, + val generatedId: Int?, + val mainKey: String? +) + +class EnteAlbumsWidgetProvider : HomeWidgetProvider() { + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray, + widgetData: SharedPreferences + ) { + appWidgetIds.forEach { widgetId -> + val views = + RemoteViews(context.packageName, R.layout.albums_widget_layout) + .apply { + val totalAlbums = + widgetData.getInt("totalAlbums", 0) + var randomNumber = -1 + var imagePath: String? = null + if (totalAlbums > 0) { + randomNumber = + (0 until totalAlbums!!).random() + imagePath = + widgetData.getString( + "albums_widget_" + + randomNumber, + null + ) + } + var imageExists: Boolean = false + if (imagePath != null) { + val imageFile = File(imagePath) + imageExists = imageFile.exists() + } + if (imageExists) { + val data = + widgetData.getString( + "albums_widget_${randomNumber}_data", + null + ) + val decoded: AlbumsFileData? = + data?.let { + Json.decodeFromString< + AlbumsFileData>(it) + } + val title = decoded?.title + val subText = decoded?.subText + val generatedId = decoded?.generatedId + val mainKey = decoded?.mainKey + + val deepLinkUri = + Uri.parse( + "albumwidget://message?generatedId=${generatedId}&mainKey=${mainKey}&homeWidget" + ) + + val pendingIntent = + HomeWidgetLaunchIntent.getActivity( + context, + MainActivity::class.java, + deepLinkUri + ) + + setOnClickPendingIntent( + R.id.widget_container, + pendingIntent + ) + + Log.d( + "EnteAlbumsWidgetProvider", + "Image exists: $imagePath" + ) + setViewVisibility( + R.id.widget_img, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_subtitle, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_title, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_overlay, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_placeholder, + View.GONE + ) + setViewVisibility( + R.id.widget_placeholder_text, + View.GONE + ) + setViewVisibility( + R.id.widget_placeholder_container, + View.GONE + ) + + val bitmap: Bitmap = + BitmapFactory.decodeFile(imagePath) + setImageViewBitmap(R.id.widget_img, bitmap) + setTextViewText(R.id.widget_title, title) + setTextViewText( + R.id.widget_subtitle, + subText + ) + } else { + // Open App on Widget Click + val pendingIntent = + HomeWidgetLaunchIntent.getActivity( + context, + MainActivity::class.java + ) + setOnClickPendingIntent( + R.id.widget_container, + pendingIntent + ) + + Log.d( + "EnteAlbumsWidgetProvider", + "Image doesn't exists" + ) + setViewVisibility( + R.id.widget_img, + View.GONE + ) + setViewVisibility( + R.id.widget_subtitle, + View.GONE + ) + setViewVisibility( + R.id.widget_title, + View.GONE + ) + setViewVisibility( + R.id.widget_overlay, + View.GONE + ) + setViewVisibility( + R.id.widget_placeholder, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_placeholder_text, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_placeholder_container, + View.VISIBLE + ) + + val drawable = + ContextCompat.getDrawable( + context, + R.drawable.ic_albums_widget + ) + val bitmap = + (drawable as BitmapDrawable).bitmap + setImageViewBitmap( + R.id.widget_placeholder, + bitmap + ) + } + } + + appWidgetManager.updateAppWidget(widgetId, views) + } + } +} diff --git a/mobile/android/app/src/main/kotlin/io/ente/photos/EnteMemoryWidgetProvider.kt b/mobile/android/app/src/main/kotlin/io/ente/photos/EnteMemoryWidgetProvider.kt index a2a4e09bf7..8e9c0f9d4a 100644 --- a/mobile/android/app/src/main/kotlin/io/ente/photos/EnteMemoryWidgetProvider.kt +++ b/mobile/android/app/src/main/kotlin/io/ente/photos/EnteMemoryWidgetProvider.kt @@ -91,10 +91,6 @@ class EnteMemoryWidgetProvider : HomeWidgetProvider() { R.id.widget_img, View.VISIBLE ) - setViewVisibility( - R.id.widget_placeholder_container, - View.VISIBLE - ) setViewVisibility( R.id.widget_subtitle, View.VISIBLE @@ -148,10 +144,6 @@ class EnteMemoryWidgetProvider : HomeWidgetProvider() { R.id.widget_img, View.GONE ) - setViewVisibility( - R.id.widget_placeholder_container, - View.GONE - ) setViewVisibility( R.id.widget_subtitle, View.GONE @@ -181,7 +173,7 @@ class EnteMemoryWidgetProvider : HomeWidgetProvider() { ContextCompat.getDrawable( context, R.drawable - .ic_home_widget_default + .ic_memories_widget ) val bitmap = (drawable as BitmapDrawable).bitmap diff --git a/mobile/android/app/src/main/kotlin/io/ente/photos/EntePeopleWidgetProvider.kt b/mobile/android/app/src/main/kotlin/io/ente/photos/EntePeopleWidgetProvider.kt new file mode 100644 index 0000000000..70f15a518c --- /dev/null +++ b/mobile/android/app/src/main/kotlin/io/ente/photos/EntePeopleWidgetProvider.kt @@ -0,0 +1,195 @@ +package io.ente.photos + +import android.appwidget.AppWidgetManager +import android.content.Context +import android.content.SharedPreferences +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable +import android.net.Uri +import android.util.Log +import android.view.View +import android.widget.RemoteViews +import androidx.core.content.ContextCompat +import es.antonborri.home_widget.HomeWidgetLaunchIntent +import es.antonborri.home_widget.HomeWidgetProvider +import java.io.File +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json + +@Serializable +data class PeopleFileData( + val title: String?, + val subText: String?, + val generatedId: Int?, + val mainKey: String? +) + +class EntePeopleWidgetProvider : HomeWidgetProvider() { + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray, + widgetData: SharedPreferences + ) { + appWidgetIds.forEach { widgetId -> + val views = + RemoteViews(context.packageName, R.layout.people_widget_layout) + .apply { + val totalPeople = + widgetData.getInt("totalPeople", 0) + var randomNumber = -1 + var imagePath: String? = null + if (totalPeople > 0) { + randomNumber = + (0 until totalPeople!!).random() + imagePath = + widgetData.getString( + "people_widget_" + + randomNumber, + null + ) + } + var imageExists: Boolean = false + if (imagePath != null) { + val imageFile = File(imagePath) + imageExists = imageFile.exists() + } + if (imageExists) { + val data = + widgetData.getString( + "people_widget_${randomNumber}_data", + null + ) + val decoded: PeopleFileData? = + data?.let { + Json.decodeFromString< + PeopleFileData>(it) + } + val title = decoded?.title + val subText = decoded?.subText + val generatedId = decoded?.generatedId + val mainKey = decoded?.mainKey + + val deepLinkUri = + Uri.parse( + "peoplewidget://message?generatedId=${generatedId}&mainKey=${mainKey}&homeWidget" + ) + + val pendingIntent = + HomeWidgetLaunchIntent.getActivity( + context, + MainActivity::class.java, + deepLinkUri + ) + + setOnClickPendingIntent( + R.id.widget_container, + pendingIntent + ) + + Log.d( + "EntePeopleWidgetProvider", + "Image exists: $imagePath" + ) + setViewVisibility( + R.id.widget_img, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_subtitle, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_title, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_overlay, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_placeholder, + View.GONE + ) + setViewVisibility( + R.id.widget_placeholder_text, + View.GONE + ) + setViewVisibility( + R.id.widget_placeholder_container, + View.GONE + ) + + val bitmap: Bitmap = + BitmapFactory.decodeFile(imagePath) + setImageViewBitmap(R.id.widget_img, bitmap) + setTextViewText(R.id.widget_title, title) + setTextViewText( + R.id.widget_subtitle, + subText + ) + } else { + // Open App on Widget Click + val pendingIntent = + HomeWidgetLaunchIntent.getActivity( + context, + MainActivity::class.java + ) + setOnClickPendingIntent( + R.id.widget_container, + pendingIntent + ) + + Log.d( + "EntePeopleWidgetProvider", + "Image doesn't exists" + ) + setViewVisibility( + R.id.widget_img, + View.GONE + ) + setViewVisibility( + R.id.widget_subtitle, + View.GONE + ) + setViewVisibility( + R.id.widget_title, + View.GONE + ) + setViewVisibility( + R.id.widget_overlay, + View.GONE + ) + setViewVisibility( + R.id.widget_placeholder, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_placeholder_text, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_placeholder_container, + View.VISIBLE + ) + + val drawable = + ContextCompat.getDrawable( + context, + R.drawable.ic_people_widget + ) + val bitmap = + (drawable as BitmapDrawable).bitmap + setImageViewBitmap( + R.id.widget_placeholder, + bitmap + ) + } + } + + appWidgetManager.updateAppWidget(widgetId, views) + } + } +} diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_albums_widget.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_albums_widget.png new file mode 100644 index 0000000000..41ba0108a9 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-hdpi/ic_albums_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_home_widget_default.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_home_widget_default.png deleted file mode 100644 index 449ea27a83..0000000000 Binary files a/mobile/android/app/src/main/res/drawable-hdpi/ic_home_widget_default.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_memories_widget.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_memories_widget.png new file mode 100644 index 0000000000..a5f302a320 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-hdpi/ic_memories_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_people_widget.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_people_widget.png new file mode 100644 index 0000000000..644b416026 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-hdpi/ic_people_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_albums_widget.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_albums_widget.png new file mode 100644 index 0000000000..a9b5792d31 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-mdpi/ic_albums_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_home_widget_default.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_home_widget_default.png deleted file mode 100644 index 4ae54e4339..0000000000 Binary files a/mobile/android/app/src/main/res/drawable-mdpi/ic_home_widget_default.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_memories_widget.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_memories_widget.png new file mode 100644 index 0000000000..fdaa8e7d53 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-mdpi/ic_memories_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_people_widget.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_people_widget.png new file mode 100644 index 0000000000..1044dc6b02 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-mdpi/ic_people_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_albums_widget.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_albums_widget.png new file mode 100644 index 0000000000..b590f07b24 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_albums_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_home_widget_default.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_home_widget_default.png deleted file mode 100644 index f158f6a086..0000000000 Binary files a/mobile/android/app/src/main/res/drawable-xhdpi/ic_home_widget_default.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_memories_widget.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_memories_widget.png new file mode 100644 index 0000000000..25db5c73b6 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_memories_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_people_widget.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_people_widget.png new file mode 100644 index 0000000000..04e74c6d0b Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_people_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_albums_widget.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_albums_widget.png new file mode 100644 index 0000000000..e501c7552b Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_albums_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_home_widget_default.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_home_widget_default.png deleted file mode 100644 index 224d849f3b..0000000000 Binary files a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_home_widget_default.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_memories_widget.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_memories_widget.png new file mode 100644 index 0000000000..f42319fb19 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_memories_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_people_widget.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_people_widget.png new file mode 100644 index 0000000000..4a9122ab54 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_people_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_albums_widget.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_albums_widget.png new file mode 100644 index 0000000000..7ffe666ae9 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_albums_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_home_widget_default.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_home_widget_default.png deleted file mode 100644 index 066e6e362a..0000000000 Binary files a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_home_widget_default.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_memories_widget.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_memories_widget.png new file mode 100644 index 0000000000..616d6f3148 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_memories_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_people_widget.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_people_widget.png new file mode 100644 index 0000000000..f935479c04 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_people_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable/albums_widget_preview.png b/mobile/android/app/src/main/res/drawable/albums_widget_preview.png new file mode 100644 index 0000000000..5a7d069ed0 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable/albums_widget_preview.png differ diff --git a/mobile/android/app/src/main/res/drawable/people_widget_preview.png b/mobile/android/app/src/main/res/drawable/people_widget_preview.png new file mode 100644 index 0000000000..7501d68098 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable/people_widget_preview.png differ diff --git a/mobile/android/app/src/main/res/layout/albums_widget_layout.xml b/mobile/android/app/src/main/res/layout/albums_widget_layout.xml new file mode 100644 index 0000000000..aa63ca753f --- /dev/null +++ b/mobile/android/app/src/main/res/layout/albums_widget_layout.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/app/src/main/res/layout/memory_widget_layout.xml b/mobile/android/app/src/main/res/layout/memory_widget_layout.xml index abda81b191..71a7b44089 100644 --- a/mobile/android/app/src/main/res/layout/memory_widget_layout.xml +++ b/mobile/android/app/src/main/res/layout/memory_widget_layout.xml @@ -65,15 +65,15 @@ android:id="@+id/widget_placeholder" android:layout_width="120dp" android:layout_height="120dp" - android:src="@drawable/ic_home_widget_default" + android:src="@drawable/ic_memories_widget" android:scaleType="fitCenter" /> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/app/src/main/res/xml/albums_widget.xml b/mobile/android/app/src/main/res/xml/albums_widget.xml new file mode 100644 index 0000000000..464b74db66 --- /dev/null +++ b/mobile/android/app/src/main/res/xml/albums_widget.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/mobile/android/app/src/main/res/xml/data_extraction_rules.xml b/mobile/android/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000000..16da40e6d4 --- /dev/null +++ b/mobile/android/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/android/app/src/main/res/xml/network_security_config.xml b/mobile/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000000..5d942a942e --- /dev/null +++ b/mobile/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,14 @@ + + + + + + + + + ente.io + + + + + diff --git a/mobile/android/app/src/main/res/xml/people_widget.xml b/mobile/android/app/src/main/res/xml/people_widget.xml new file mode 100644 index 0000000000..a6c37ba398 --- /dev/null +++ b/mobile/android/app/src/main/res/xml/people_widget.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/mobile/assets/2.0x/albums-widget-static.png b/mobile/assets/2.0x/albums-widget-static.png new file mode 100644 index 0000000000..00bb2ba52a Binary files /dev/null and b/mobile/assets/2.0x/albums-widget-static.png differ diff --git a/mobile/assets/2.0x/memories-widget-static.png b/mobile/assets/2.0x/memories-widget-static.png new file mode 100644 index 0000000000..1e079f1e99 Binary files /dev/null and b/mobile/assets/2.0x/memories-widget-static.png differ diff --git a/mobile/assets/2.0x/people-widget-static.png b/mobile/assets/2.0x/people-widget-static.png new file mode 100644 index 0000000000..1e619b26e9 Binary files /dev/null and b/mobile/assets/2.0x/people-widget-static.png differ diff --git a/mobile/assets/3.0x/albums-widget-static.png b/mobile/assets/3.0x/albums-widget-static.png new file mode 100644 index 0000000000..9826a8d8c2 Binary files /dev/null and b/mobile/assets/3.0x/albums-widget-static.png differ diff --git a/mobile/assets/3.0x/memories-widget-static.png b/mobile/assets/3.0x/memories-widget-static.png new file mode 100644 index 0000000000..dc0cdef537 Binary files /dev/null and b/mobile/assets/3.0x/memories-widget-static.png differ diff --git a/mobile/assets/3.0x/people-widget-static.png b/mobile/assets/3.0x/people-widget-static.png new file mode 100644 index 0000000000..aac3ee642c Binary files /dev/null and b/mobile/assets/3.0x/people-widget-static.png differ diff --git a/mobile/assets/albums-widget-static.png b/mobile/assets/albums-widget-static.png new file mode 100644 index 0000000000..9e6ddcf832 Binary files /dev/null and b/mobile/assets/albums-widget-static.png differ diff --git a/mobile/assets/icons/albums-widget-icon.svg b/mobile/assets/icons/albums-widget-icon.svg new file mode 100644 index 0000000000..d42448bcef --- /dev/null +++ b/mobile/assets/icons/albums-widget-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile/assets/icons/memories-widget-icon.svg b/mobile/assets/icons/memories-widget-icon.svg new file mode 100644 index 0000000000..7a72222cc7 --- /dev/null +++ b/mobile/assets/icons/memories-widget-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/mobile/assets/icons/past-year-memory-icon.svg b/mobile/assets/icons/past-year-memory-icon.svg new file mode 100644 index 0000000000..4c50152714 --- /dev/null +++ b/mobile/assets/icons/past-year-memory-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile/assets/icons/people-widget-icon.svg b/mobile/assets/icons/people-widget-icon.svg new file mode 100644 index 0000000000..3cd1ee3972 --- /dev/null +++ b/mobile/assets/icons/people-widget-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile/assets/icons/smart-memory-icon.svg b/mobile/assets/icons/smart-memory-icon.svg new file mode 100644 index 0000000000..cdc2d89b59 --- /dev/null +++ b/mobile/assets/icons/smart-memory-icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/mobile/assets/memories-widget-static.png b/mobile/assets/memories-widget-static.png new file mode 100644 index 0000000000..4f7f546ad7 Binary files /dev/null and b/mobile/assets/memories-widget-static.png differ diff --git a/mobile/assets/people-widget-static.png b/mobile/assets/people-widget-static.png new file mode 100644 index 0000000000..038f8eeb81 Binary files /dev/null and b/mobile/assets/people-widget-static.png differ diff --git a/mobile/ios/EnteAlbumWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/mobile/ios/EnteAlbumWidget/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/mobile/ios/EnteAlbumWidget/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EnteAlbumWidget/Assets.xcassets/Contents.json b/mobile/ios/EnteAlbumWidget/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/mobile/ios/EnteAlbumWidget/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EnteAlbumWidget/Assets.xcassets/IconGreen.appiconset/Contents.json b/mobile/ios/EnteAlbumWidget/Assets.xcassets/IconGreen.appiconset/Contents.json new file mode 100644 index 0000000000..2305880107 --- /dev/null +++ b/mobile/ios/EnteAlbumWidget/Assets.xcassets/IconGreen.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EnteAlbumWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/mobile/ios/EnteAlbumWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/mobile/ios/EnteAlbumWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EnteAlbumWidget/EnteAlbumWidget.swift b/mobile/ios/EnteAlbumWidget/EnteAlbumWidget.swift new file mode 100644 index 0000000000..0a4f9d6e76 --- /dev/null +++ b/mobile/ios/EnteAlbumWidget/EnteAlbumWidget.swift @@ -0,0 +1,277 @@ +// +// EnteAlbumWidget.swift +// EnteAlbumWidget +// +// Created by Prateek Sunal on 5/15/25. +// Copyright © 2025 The Chromium Authors. All rights reserved. +// + +import SwiftUI +import UIKit +import WidgetKit + +private let widgetGroupId = "group.io.ente.frame.EnteMemoryWidget" + +struct Provider: TimelineProvider { + let minutes = 15 + let data = UserDefaults(suiteName: widgetGroupId) + + func placeholder(in _: Context) -> FileEntry { + FileEntry( + date: Date(), index: nil, imageData: nil, title: "Title", subTitle: "Sub Title", + generatedId: nil, mainKey: nil) + } + + func getSnapshot(in _: Context, completion: @escaping (FileEntry) -> Void) { + let entry = FileEntry( + date: Date(), index: -2, imageData: nil, title: "Favorites", + subTitle: "May 3, 2021", + generatedId: nil, mainKey: nil) + completion(entry) + } + + func getTimeline(in _: Context, completion: @escaping (Timeline) -> Void) { + var entries: [FileEntry] = [] + + // Generate a timeline consisting of five entries an hour apart, starting from the current date. + let currentDate = Calendar.current.nextDate( + after: Date(), matching: DateComponents(second: 0), matchingPolicy: .nextTime, + direction: .backward + )! + + var totalAlbums = + data?.integer(forKey: "totalAlbums") + + if totalAlbums != nil && totalAlbums! > 0 { + let count = totalAlbums! > 5 ? 5 : totalAlbums + for offset in 0.. WidgetRelevances { + // // Generate a list containing the contexts this widget is relevant in. + // } +} + +struct FileEntry: TimelineEntry { + let date: Date + let index: Int? + let imageData: String? + let title: String? + let subTitle: String? + var generatedId: Int? + var mainKey: String? +} + +struct EnteAlbumWidgetEntryView: View { + var entry: Provider.Entry + let defaultBase64Image = + "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" + let defaultBase64Preview = + "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" + + let data = UserDefaults.init(suiteName: widgetGroupId) + + var body: some View { + GeometryReader { geometry in + ZStack { + if let imageData = entry.imageData, + let uiImage = UIImage(contentsOfFile: imageData) + { + Image(uiImage: uiImage) + .resizable() + .backwardWidgetFullColorRenderingMode() + .aspectRatio(contentMode: .fill) + .frame(width: geometry.size.width, height: geometry.size.height) + .overlay( + LinearGradient( + gradient: Gradient(colors: [Color.black.opacity(0.7), Color.clear]), + startPoint: .bottom, + endPoint: .top + ) + + .frame(height: geometry.size.height * 0.4) + .frame(maxHeight: .infinity, alignment: .bottom) + .backwardWidgetAccentable(true) + ) + .overlay( + VStack(alignment: .leading, spacing: 2) { + Text(entry.title ?? "").font( + .custom("Inter", size: 14, relativeTo: .caption) + ) // Custom with fallback + .bold() + .foregroundStyle(.white) + .shadow(radius: 20) + Text(entry.subTitle ?? "") + .font(.custom("Inter", size: 12, relativeTo: .caption2)) + .foregroundStyle(.white) + .shadow(radius: 20) + } + .padding(.leading, geometry.size.width * 0.05) + .padding(.bottom, geometry.size.height * 0.05), + alignment: .bottomLeading + ) + } else if entry.index == -2 { + if let data = Data( + base64Encoded: defaultBase64Preview + ), + let uiImage = UIImage(data: data) + { + Image(uiImage: uiImage) + .resizable() + .backwardWidgetFullColorRenderingMode() + .aspectRatio(contentMode: .fill) + .frame(width: geometry.size.width, height: geometry.size.height) + .overlay( + LinearGradient( + gradient: Gradient(colors: [ + Color.black.opacity(0.7), Color.clear, + ]), + startPoint: .bottom, + endPoint: .top + ) + + .frame(height: geometry.size.height * 0.4) + .frame(maxHeight: .infinity, alignment: .bottom) + .backwardWidgetAccentable(true) + ) + .overlay( + VStack(alignment: .leading, spacing: 2) { + Text(entry.title ?? "").font( + .custom("Inter", size: 14, relativeTo: .caption) + ) // Custom with fallback + .bold() + .foregroundStyle(.white) + .shadow(radius: 20) + Text(entry.subTitle ?? "") + .font(.custom("Inter", size: 12, relativeTo: .caption2)) + .foregroundStyle(.white) + .shadow(radius: 20) + } + .padding(.leading, geometry.size.width * 0.05) + .padding(.bottom, geometry.size.height * 0.05), + alignment: .bottomLeading + ) + } + } else if let imgData = Data( + base64Encoded: defaultBase64Image), + let uiImage = UIImage(data: imgData) + { + VStack(spacing: 8) { + Spacer() + Image(uiImage: uiImage) + .resizable() + .backwardWidgetFullColorRenderingMode() + .aspectRatio(contentMode: .fit) + .padding(8) + + Text("Go to Settings -> General to customise the widget") + .font(.custom("Inter", size: 12, relativeTo: .caption)) + .foregroundStyle(.white) // Tint-aware color + .multilineTextAlignment(.center) + .padding(.bottom, 12) + .padding(.horizontal, 8) + .backwardWidgetAccentable(true) + Spacer() + } + .frame(width: geometry.size.width, height: geometry.size.height) + } else { + Color.gray + } + } + .clipped() + .edgesIgnoringSafeArea(.all) + .widgetURL( + URL( + string: + "albumwidget://message?generatedId=\(entry.generatedId != nil ? String(entry.generatedId!) : "nan")&mainKey=\(entry.mainKey != nil ? entry.mainKey! : "nan")&homeWidget" + ) + ) + } + } +} + +struct EnteAlbumWidget: Widget { + let kind: String = "EnteAlbumWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: Provider()) { entry in + if #available(iOS 17.0, *) { + EnteAlbumWidgetEntryView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } else { + EnteAlbumWidgetEntryView(entry: entry) + .padding() + .background() + } + } + .configurationDisplayName("Albums") + .description("See photos from selected albums including your favorites") + .contentMarginsDisabled() + } +} + +#Preview(as: .systemSmall) { + EnteAlbumWidget() +} timeline: { + FileEntry( + date: .now, index: -2, imageData: nil, title: nil, subTitle: nil, generatedId: nil, + mainKey: nil) + FileEntry( + date: .now, index: -2, imageData: nil, title: nil, subTitle: nil, generatedId: nil, + mainKey: nil) +} + +extension View { + @ViewBuilder + func backwardWidgetAccentable(_ accentable: Bool = true) -> some View { + if #available(iOS 16.0, *) { + self.widgetAccentable(accentable) + } else { + self + } + } +} + +extension Image { + @ViewBuilder + func backwardWidgetAccentedRenderingMode(_ isAccentedRenderingMode: Bool = true) -> some View { + if #available(iOS 18.0, *) { + self.widgetAccentedRenderingMode(isAccentedRenderingMode ? .accented : .fullColor) + } else { + self + } + } + + @ViewBuilder + func backwardWidgetFullColorRenderingMode() -> some View { + backwardWidgetAccentedRenderingMode(false) + } +} diff --git a/mobile/ios/EnteAlbumWidget/EnteAlbumWidgetBundle.swift b/mobile/ios/EnteAlbumWidget/EnteAlbumWidgetBundle.swift new file mode 100644 index 0000000000..447511b8ae --- /dev/null +++ b/mobile/ios/EnteAlbumWidget/EnteAlbumWidgetBundle.swift @@ -0,0 +1,17 @@ +// +// EnteAlbumWidgetBundle.swift +// EnteAlbumWidget +// +// Created by Prateek Sunal on 5/15/25. +// Copyright © 2025 The Chromium Authors. All rights reserved. +// + +import WidgetKit +import SwiftUI + +@main +struct EnteAlbumWidgetBundle: WidgetBundle { + var body: some Widget { + EnteAlbumWidget() + } +} diff --git a/mobile/ios/EnteAlbumWidget/Info.plist b/mobile/ios/EnteAlbumWidget/Info.plist new file mode 100644 index 0000000000..0f118fb75e --- /dev/null +++ b/mobile/ios/EnteAlbumWidget/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/mobile/ios/EnteAlbumWidgetExtension.entitlements b/mobile/ios/EnteAlbumWidgetExtension.entitlements new file mode 100644 index 0000000000..866b9602db --- /dev/null +++ b/mobile/ios/EnteAlbumWidgetExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.io.ente.frame.EnteMemoryWidget + + + diff --git a/mobile/ios/EnteMemoryWidget/EnteMemoryWidget.swift b/mobile/ios/EnteMemoryWidget/EnteMemoryWidget.swift index 0d7d3790e1..4382deab9b 100644 --- a/mobile/ios/EnteMemoryWidget/EnteMemoryWidget.swift +++ b/mobile/ios/EnteMemoryWidget/EnteMemoryWidget.swift @@ -13,7 +13,7 @@ import WidgetKit private let widgetGroupId = "group.io.ente.frame.EnteMemoryWidget" struct Provider: TimelineProvider { - let X = 15 + let minutes = 15 let data = UserDefaults(suiteName: widgetGroupId) func placeholder(in _: Context) -> FileEntry { @@ -47,7 +47,7 @@ struct Provider: TimelineProvider { for offset in 0.. General to customise the widget") + .font(.custom("Inter", size: 12, relativeTo: .caption)) .foregroundStyle(.white) // Tint-aware color .multilineTextAlignment(.center) .padding(.bottom, 12) + .padding(.horizontal, 8) .backwardWidgetAccentable(true) Spacer() } @@ -229,8 +230,8 @@ struct EnteMemoryWidget: Widget { .background() } } - .configurationDisplayName("Your memories") - .description("See special moments from the past.") + .configurationDisplayName("Memories") + .description("See special moments from the past") .contentMarginsDisabled() } } diff --git a/mobile/ios/EntePeopleWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/mobile/ios/EntePeopleWidget/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/mobile/ios/EntePeopleWidget/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EntePeopleWidget/Assets.xcassets/Contents.json b/mobile/ios/EntePeopleWidget/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/mobile/ios/EntePeopleWidget/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EntePeopleWidget/Assets.xcassets/IconGreen.appiconset/Contents.json b/mobile/ios/EntePeopleWidget/Assets.xcassets/IconGreen.appiconset/Contents.json new file mode 100644 index 0000000000..2305880107 --- /dev/null +++ b/mobile/ios/EntePeopleWidget/Assets.xcassets/IconGreen.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EntePeopleWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/mobile/ios/EntePeopleWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/mobile/ios/EntePeopleWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EntePeopleWidget/EntePeopleWidget.swift b/mobile/ios/EntePeopleWidget/EntePeopleWidget.swift new file mode 100644 index 0000000000..951c75767f --- /dev/null +++ b/mobile/ios/EntePeopleWidget/EntePeopleWidget.swift @@ -0,0 +1,277 @@ +// +// EntePeopleWidget.swift +// EntePeopleWidget +// +// Created by Prateek Sunal on 5/15/25. +// Copyright © 2025 The Chromium Authors. All rights reserved. +// + +import SwiftUI +import UIKit +import WidgetKit + +private let widgetGroupId = "group.io.ente.frame.EnteMemoryWidget" + +struct Provider: TimelineProvider { + let minutes = 15 + let data = UserDefaults(suiteName: widgetGroupId) + + func placeholder(in _: Context) -> FileEntry { + FileEntry( + date: Date(), index: nil, imageData: nil, title: "Title", subTitle: "Sub Title", + generatedId: nil, mainKey: nil) + } + + func getSnapshot(in _: Context, completion: @escaping (FileEntry) -> Void) { + let entry = FileEntry( + date: Date(), index: -2, imageData: nil, title: "Jane Fonda", + subTitle: "Sep 23, 2021", + generatedId: nil, mainKey: nil) + completion(entry) + } + + func getTimeline(in _: Context, completion: @escaping (Timeline) -> Void) { + var entries: [FileEntry] = [] + + // Generate a timeline consisting of five entries an hour apart, starting from the current date. + let currentDate = Calendar.current.nextDate( + after: Date(), matching: DateComponents(second: 0), matchingPolicy: .nextTime, + direction: .backward + )! + + var totalPeople = + data?.integer(forKey: "totalPeople") + + if totalPeople != nil && totalPeople! > 0 { + let count = totalPeople! > 5 ? 5 : totalPeople + for offset in 0.. WidgetRelevances { + // // Generate a list containing the contexts this widget is relevant in. + // } +} + +struct FileEntry: TimelineEntry { + let date: Date + let index: Int? + let imageData: String? + let title: String? + let subTitle: String? + var generatedId: Int? + var mainKey: String? +} + +struct EntePeopleWidgetEntryView: View { + var entry: Provider.Entry + let defaultBase64Image = + "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" + let defaultBase64Preview = + "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" + + let data = UserDefaults.init(suiteName: widgetGroupId) + + var body: some View { + GeometryReader { geometry in + ZStack { + if let imageData = entry.imageData, + let uiImage = UIImage(contentsOfFile: imageData) + { + Image(uiImage: uiImage) + .resizable() + .backwardWidgetFullColorRenderingMode() + .aspectRatio(contentMode: .fill) + .frame(width: geometry.size.width, height: geometry.size.height) + .overlay( + LinearGradient( + gradient: Gradient(colors: [Color.black.opacity(0.7), Color.clear]), + startPoint: .bottom, + endPoint: .top + ) + + .frame(height: geometry.size.height * 0.4) + .frame(maxHeight: .infinity, alignment: .bottom) + .backwardWidgetAccentable(true) + ) + .overlay( + VStack(alignment: .leading, spacing: 2) { + Text(entry.title ?? "").font( + .custom("Inter", size: 14, relativeTo: .caption) + ) // Custom with fallback + .bold() + .foregroundStyle(.white) + .shadow(radius: 20) + Text(entry.subTitle ?? "") + .font(.custom("Inter", size: 12, relativeTo: .caption2)) + .foregroundStyle(.white) + .shadow(radius: 20) + } + .padding(.leading, geometry.size.width * 0.05) + .padding(.bottom, geometry.size.height * 0.05), + alignment: .bottomLeading + ) + } else if entry.index == -2 { + if let data = Data( + base64Encoded: defaultBase64Preview + ), + let uiImage = UIImage(data: data) + { + Image(uiImage: uiImage) + .resizable() + .backwardWidgetFullColorRenderingMode() + .aspectRatio(contentMode: .fill) + .frame(width: geometry.size.width, height: geometry.size.height) + .overlay( + LinearGradient( + gradient: Gradient(colors: [ + Color.black.opacity(0.7), Color.clear, + ]), + startPoint: .bottom, + endPoint: .top + ) + + .frame(height: geometry.size.height * 0.4) + .frame(maxHeight: .infinity, alignment: .bottom) + .backwardWidgetAccentable(true) + ) + .overlay( + VStack(alignment: .leading, spacing: 2) { + Text(entry.title ?? "").font( + .custom("Inter", size: 14, relativeTo: .caption) + ) // Custom with fallback + .bold() + .foregroundStyle(.white) + .shadow(radius: 20) + Text(entry.subTitle ?? "") + .font(.custom("Inter", size: 12, relativeTo: .caption2)) + .foregroundStyle(.white) + .shadow(radius: 20) + } + .padding(.leading, geometry.size.width * 0.05) + .padding(.bottom, geometry.size.height * 0.05), + alignment: .bottomLeading + ) + } + } else if let data = Data( + base64Encoded: defaultBase64Image), + let uiImage = UIImage(data: data) + { + VStack(spacing: 8) { + Spacer() + Image(uiImage: uiImage) + .resizable() + .backwardWidgetFullColorRenderingMode() + .aspectRatio(contentMode: .fit) + .padding(8) + + Text("Go to Settings -> General to customise the widget") + .font(.custom("Inter", size: 12, relativeTo: .caption)) + .foregroundStyle(.white) // Tint-aware color + .multilineTextAlignment(.center) + .padding(.bottom, 12) + .padding(.horizontal, 8) + .backwardWidgetAccentable(true) + Spacer() + } + .frame(width: geometry.size.width, height: geometry.size.height) + } else { + Color.gray + } + } + .clipped() + .edgesIgnoringSafeArea(.all) + .widgetURL( + URL( + string: + "peoplewidget://message?generatedId=\(entry.generatedId != nil ? String(entry.generatedId!) : "nan")&mainKey=\(entry.mainKey != nil ? entry.mainKey! : "nan")&homeWidget" + ) + ) + } + } +} + +struct EntePeopleWidget: Widget { + let kind: String = "EntePeopleWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: Provider()) { entry in + if #available(iOS 17.0, *) { + EntePeopleWidgetEntryView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } else { + EntePeopleWidgetEntryView(entry: entry) + .padding() + .background() + } + } + .configurationDisplayName("People Widget") + .description("See photos of your favorite people") + .contentMarginsDisabled() + } +} + +#Preview(as: .systemSmall) { + EntePeopleWidget() +} timeline: { + FileEntry( + date: .now, index: -2, imageData: nil, title: nil, subTitle: nil, generatedId: nil, + mainKey: nil) + FileEntry( + date: .now, index: -2, imageData: nil, title: nil, subTitle: nil, generatedId: nil, + mainKey: nil) +} + +extension View { + @ViewBuilder + func backwardWidgetAccentable(_ accentable: Bool = true) -> some View { + if #available(iOS 16.0, *) { + self.widgetAccentable(accentable) + } else { + self + } + } +} + +extension Image { + @ViewBuilder + func backwardWidgetAccentedRenderingMode(_ isAccentedRenderingMode: Bool = true) -> some View { + if #available(iOS 18.0, *) { + self.widgetAccentedRenderingMode(isAccentedRenderingMode ? .accented : .fullColor) + } else { + self + } + } + + @ViewBuilder + func backwardWidgetFullColorRenderingMode() -> some View { + backwardWidgetAccentedRenderingMode(false) + } +} diff --git a/mobile/ios/EntePeopleWidget/EntePeopleWidgetBundle.swift b/mobile/ios/EntePeopleWidget/EntePeopleWidgetBundle.swift new file mode 100644 index 0000000000..e68b752d45 --- /dev/null +++ b/mobile/ios/EntePeopleWidget/EntePeopleWidgetBundle.swift @@ -0,0 +1,17 @@ +// +// EntePeopleWidgetBundle.swift +// EntePeopleWidget +// +// Created by Prateek Sunal on 5/15/25. +// Copyright © 2025 The Chromium Authors. All rights reserved. +// + +import WidgetKit +import SwiftUI + +@main +struct EntePeopleWidgetBundle: WidgetBundle { + var body: some Widget { + EntePeopleWidget() + } +} diff --git a/mobile/ios/EntePeopleWidget/Info.plist b/mobile/ios/EntePeopleWidget/Info.plist new file mode 100644 index 0000000000..0f118fb75e --- /dev/null +++ b/mobile/ios/EntePeopleWidget/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/mobile/ios/EntePeopleWidgetExtension.entitlements b/mobile/ios/EntePeopleWidgetExtension.entitlements new file mode 100644 index 0000000000..866b9602db --- /dev/null +++ b/mobile/ios/EntePeopleWidgetExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.io.ente.frame.EnteMemoryWidget + + + diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 9d91a4c09e..3659a25b77 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -75,6 +75,8 @@ PODS: - Flutter - flutter_sodium (0.0.1): - Flutter + - flutter_timezone (0.0.1): + - Flutter - fluttertoast (0.0.2): - Flutter - GoogleDataTransport (10.1.0): @@ -261,6 +263,7 @@ DEPENDENCIES: - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_sodium (from `.symlinks/plugins/flutter_sodium/ios`) + - flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - home_widget (from `.symlinks/plugins/home_widget/ios`) - image_editor_common (from `.symlinks/plugins/image_editor_common/ios`) @@ -301,7 +304,7 @@ DEPENDENCIES: - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) SPEC REPOS: - https://github.com/ente-io/ffmpeg-kit-custom-repo-ios: + https://github.com/ente-io/ffmpeg-kit-custom-repo-ios.git: - ffmpeg_kit_custom trunk: - Firebase @@ -362,6 +365,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_secure_storage/ios" flutter_sodium: :path: ".symlinks/plugins/flutter_sodium/ios" + flutter_timezone: + :path: ".symlinks/plugins/flutter_timezone/ios" fluttertoast: :path: ".symlinks/plugins/fluttertoast/ios" home_widget: @@ -440,82 +445,83 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: - app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7 - background_fetch: 94b36ee293e82972852dba8ede1fbcd3bd3d9d57 - battery_info: 83f3aae7be2fccefab1d2bf06b8aa96f11c8bcdd - connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd - cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c - dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1 - device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89 + app_links: f3e17e4ee5e357b39d8b95290a9b2c299fca71c6 + background_fetch: 39f11371c0dce04b001c4bfd5e782bcccb0a85e2 + battery_info: b6c551049266af31556b93c9d9b9452cfec0219f + connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d + cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba + dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14 + device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 ffmpeg_kit_custom: 682b4f2f1ff1f8abae5a92f6c3540f2441d5be99 - ffmpeg_kit_flutter: 915b345acc97d4142e8a9a8549d177ff10f043f5 - file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 + ffmpeg_kit_flutter: 9dce4803991478c78c6fb9f972703301101095fe + file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf - firebase_core: 6cbed78b4f298ed103a9fd034e6dbc846320480f - firebase_messaging: 5e0adf2eb18b0ee59aa0c109314c091a0497ecac + firebase_core: 6e223dfa350b2edceb729cea505eaaef59330682 + firebase_messaging: 07fde77ae28c08616a1d4d870450efc2b38cf40d FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629 FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917 FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_email_sender: aa1e9772696691d02cd91fea829856c11efb8e58 - flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 - flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 - flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100 - flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145 - flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418 - flutter_sodium: 7e4621538491834eba53bd524547854bcbbd6987 - fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 + flutter_email_sender: e03bdda7637bcd3539bfe718fddd980e9508efaa + flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e + flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 + flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f + flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a + flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be + flutter_sodium: a00383520fc689c688b66fd3092984174712493e + flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282 + fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d - home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f - image_editor_common: 3de87e7c4804f4ae24c8f8a998362b98c105cac1 - in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6 - integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e - launcher_icon_switcher: 84c218d233505aa7d8655d8fa61a3ba802c022da + home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 + image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43 + in_app_purchase_storekit: a1ce04056e23eecc666b086040239da7619cd783 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + launcher_icon_switcher: 8e0ad2131a20c51c1dd939896ee32e70cd845b37 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 - local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 - local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451 + local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3 + local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d - maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45 - media_extension: 671e2567880d96c95c65c9a82ccceed8f2e309fd - media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 - media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 - motion_sensors: 741e702c17467b9569a92165dda8d4d88c6167f1 - motionphoto: 23e2aeb5c6380112f69468d71f970fa7438e5ed1 - move_to_background: 7e3467dd2a1d1013e98c9c1cb93fd53cd7ef9d84 + maps_launcher: 2e5b6a2d664ec6c27f82ffa81b74228d770ab203 + media_extension: 6618f07abd762cdbfaadf1b0c56a287e820f0c84 + media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 + media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e + motion_sensors: 03f55b7c637a7e365a0b5f9697a449f9059d5d91 + motionphoto: 8b65ce50c7d7ff3c767534fc3768b2eed9ac24e4 + move_to_background: cd3091014529ec7829e342ad2d75c0a11f4378a5 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - native_video_player: e363dd14f6a498ad8a8f7e6486a0db046ad19f13 - objective_c: 89e720c30d716b036faf9c9684022048eee1eee2 - onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2 + native_video_player: 5d36066807b680e181473e6890dde643ac85380d + objective_c: 77e887b5ba1827970907e10e832eec1683f3431d + onnxruntime: e7c2ae44385191eaad5ae64c935a72debaddc997 onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c onnxruntime-objc: b6fab0f1787aa6f7190c2013f03037df4718bd8b - open_mail_app: 7314a609e88eed22d53671279e189af7a0ab0f11 + open_mail_app: 70273c53f768beefdafbe310c3d9086e4da3cb02 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 - package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413 - privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4 + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a + privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 rust_lib_photos: 04d3901908d2972192944083310b65abf410774c + receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1 SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854 - sentry_flutter: 942017adbe00f963061cb11ec260414a990b7a42 - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + sentry_flutter: 6a134f9d381e49f22ea25a67736cf0cf4d02ec9c + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 - sqlite3_flutter_libs: 3c323550ef3b928bc0aa9513c841e45a7d242832 - system_info_plus: 555ce7047fbbf29154726db942ae785c29211740 - ua_client_hints: 92fe0d139619b73ec9fcb46cc7e079a26178f586 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b - video_thumbnail: 584ccfa55d8fd2f3d5507218b0a18d84c839c620 - volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12 - wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49 + sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa + system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa + ua_client_hints: aeabd123262c087f0ce151ef96fa3ab77bfc8b38 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 + video_thumbnail: 94ba6705afbaa120b77287080424930f23ea0c40 + volume_controller: 2e3de73d6e7e81a0067310d17fb70f2f86d71ac7 + wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56 PODFILE CHECKSUM: a8ef88ad74ba499756207e7592c6071a96756d18 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 4cd76b0faf..68c92379c4 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -16,6 +16,12 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + CEE166242DD5E7820012CF61 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DACD83C2B755B0600BA9516 /* WidgetKit.framework */; }; + CEE166252DD5E7820012CF61 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DACD83E2B755B0600BA9516 /* SwiftUI.framework */; }; + CEE166302DD5E7830012CF61 /* EnteAlbumWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = CEE166232DD5E7820012CF61 /* EnteAlbumWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + CEE1667C2DD5F6F20012CF61 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DACD83C2B755B0600BA9516 /* WidgetKit.framework */; }; + CEE1667D2DD5F6F20012CF61 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DACD83E2B755B0600BA9516 /* SwiftUI.framework */; }; + CEE166882DD5F6F30012CF61 /* EntePeopleWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = CEE1667B2DD5F6F20012CF61 /* EntePeopleWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; CEE6BE702D7AE7FD00E4048B /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DACD83C2B755B0600BA9516 /* WidgetKit.framework */; }; CEE6BE712D7AE7FD00E4048B /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DACD83E2B755B0600BA9516 /* SwiftUI.framework */; }; CEE6BE7C2D7AE7FE00E4048B /* EnteMemoryWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = CEE6BE6F2D7AE7FD00E4048B /* EnteMemoryWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -23,6 +29,20 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + CEE1662E2DD5E7830012CF61 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = CEE166222DD5E7820012CF61; + remoteInfo = EnteAlbumWidgetExtension; + }; + CEE166862DD5F6F30012CF61 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = CEE1667A2DD5F6F20012CF61; + remoteInfo = EntePeopleWidgetExtension; + }; CEE6BE7A2D7AE7FE00E4048B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; @@ -39,7 +59,9 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( + CEE166302DD5E7830012CF61 /* EnteAlbumWidgetExtension.appex in Embed Foundation Extensions */, CEE6BE7C2D7AE7FE00E4048B /* EnteMemoryWidgetExtension.appex in Embed Foundation Extensions */, + CEE166882DD5F6F30012CF61 /* EntePeopleWidgetExtension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -79,6 +101,10 @@ A78E51A260432466D4C456A9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; BB097BB5EB0EEB41344338D2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; CE93A9062D808893005CD942 /* EnteMemoryWidgetExtensionDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EnteMemoryWidgetExtensionDebug.entitlements; sourceTree = ""; }; + CEE166232DD5E7820012CF61 /* EnteAlbumWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = EnteAlbumWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + CEE166362DD5E7950012CF61 /* EnteAlbumWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EnteAlbumWidgetExtension.entitlements; sourceTree = ""; }; + CEE1667B2DD5F6F20012CF61 /* EntePeopleWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = EntePeopleWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + CEE1668E2DD5F70F0012CF61 /* EntePeopleWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EntePeopleWidgetExtension.entitlements; sourceTree = ""; }; CEE6BE6F2D7AE7FD00E4048B /* EnteMemoryWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = EnteMemoryWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; CEE6BE822D7AE8C700E4048B /* EnteMemoryWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EnteMemoryWidgetExtension.entitlements; sourceTree = ""; }; DA8D6672273BBB59007651D4 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; @@ -86,6 +112,20 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + CEE166342DD5E7830012CF61 /* Exceptions for "EnteAlbumWidget" folder in "EnteAlbumWidgetExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = CEE166222DD5E7820012CF61 /* EnteAlbumWidgetExtension */; + }; + CEE1668C2DD5F6F30012CF61 /* Exceptions for "EntePeopleWidget" folder in "EntePeopleWidgetExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = CEE1667A2DD5F6F20012CF61 /* EntePeopleWidgetExtension */; + }; CEE6BE802D7AE7FE00E4048B /* Exceptions for "EnteMemoryWidget" folder in "EnteMemoryWidgetExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -96,6 +136,30 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + CEE166262DD5E7820012CF61 /* EnteAlbumWidget */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + CEE166342DD5E7830012CF61 /* Exceptions for "EnteAlbumWidget" folder in "EnteAlbumWidgetExtension" target */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = EnteAlbumWidget; + sourceTree = ""; + }; + CEE1667E2DD5F6F20012CF61 /* EntePeopleWidget */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + CEE1668C2DD5F6F30012CF61 /* Exceptions for "EntePeopleWidget" folder in "EntePeopleWidgetExtension" target */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = EntePeopleWidget; + sourceTree = ""; + }; CEE6BE722D7AE7FD00E4048B /* EnteMemoryWidget */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -120,6 +184,24 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CEE166202DD5E7820012CF61 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CEE166252DD5E7820012CF61 /* SwiftUI.framework in Frameworks */, + CEE166242DD5E7820012CF61 /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CEE166782DD5F6F20012CF61 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CEE1667D2DD5F6F20012CF61 /* SwiftUI.framework in Frameworks */, + CEE1667C2DD5F6F20012CF61 /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; CEE6BE6C2D7AE7FD00E4048B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -146,12 +228,16 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( + CEE1668E2DD5F70F0012CF61 /* EntePeopleWidgetExtension.entitlements */, + CEE166362DD5E7950012CF61 /* EnteAlbumWidgetExtension.entitlements */, CE93A9062D808893005CD942 /* EnteMemoryWidgetExtensionDebug.entitlements */, CEE6BE822D7AE8C700E4048B /* EnteMemoryWidgetExtension.entitlements */, 2772189F270F596900FFE3CC /* GoogleService-Info.plist */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, CEE6BE722D7AE7FD00E4048B /* EnteMemoryWidget */, + CEE166262DD5E7820012CF61 /* EnteAlbumWidget */, + CEE1667E2DD5F6F20012CF61 /* EntePeopleWidget */, 97C146EF1CF9000F007C117D /* Products */, AC6CA265BB505D982CB00391 /* Pods */, C6A22658E77FF012720BEDDA /* Frameworks */, @@ -163,6 +249,8 @@ children = ( 97C146EE1CF9000F007C117D /* Runner.app */, CEE6BE6F2D7AE7FD00E4048B /* EnteMemoryWidgetExtension.appex */, + CEE166232DD5E7820012CF61 /* EnteAlbumWidgetExtension.appex */, + CEE1667B2DD5F6F20012CF61 /* EntePeopleWidgetExtension.appex */, ); name = Products; sourceTree = ""; @@ -235,12 +323,54 @@ ); dependencies = ( CEE6BE7B2D7AE7FE00E4048B /* PBXTargetDependency */, + CEE1662F2DD5E7830012CF61 /* PBXTargetDependency */, + CEE166872DD5F6F30012CF61 /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; + CEE166222DD5E7820012CF61 /* EnteAlbumWidgetExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = CEE166352DD5E7830012CF61 /* Build configuration list for PBXNativeTarget "EnteAlbumWidgetExtension" */; + buildPhases = ( + CEE1661F2DD5E7820012CF61 /* Sources */, + CEE166202DD5E7820012CF61 /* Frameworks */, + CEE166212DD5E7820012CF61 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + CEE166262DD5E7820012CF61 /* EnteAlbumWidget */, + ); + name = EnteAlbumWidgetExtension; + productName = EnteAlbumWidgetExtension; + productReference = CEE166232DD5E7820012CF61 /* EnteAlbumWidgetExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; + CEE1667A2DD5F6F20012CF61 /* EntePeopleWidgetExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = CEE1668D2DD5F6F30012CF61 /* Build configuration list for PBXNativeTarget "EntePeopleWidgetExtension" */; + buildPhases = ( + CEE166772DD5F6F20012CF61 /* Sources */, + CEE166782DD5F6F20012CF61 /* Frameworks */, + CEE166792DD5F6F20012CF61 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + CEE1667E2DD5F6F20012CF61 /* EntePeopleWidget */, + ); + name = EntePeopleWidgetExtension; + productName = EntePeopleWidgetExtension; + productReference = CEE1667B2DD5F6F20012CF61 /* EntePeopleWidgetExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; CEE6BE6E2D7AE7FD00E4048B /* EnteMemoryWidgetExtension */ = { isa = PBXNativeTarget; buildConfigurationList = CEE6BE812D7AE7FE00E4048B /* Build configuration list for PBXNativeTarget "EnteMemoryWidgetExtension" */; @@ -267,7 +397,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1620; + LastSwiftUpdateCheck = 1630; LastUpgradeCheck = 1510; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { @@ -276,6 +406,12 @@ LastSwiftMigration = 1100; ProvisioningStyle = Automatic; }; + CEE166222DD5E7820012CF61 = { + CreatedOnToolsVersion = 16.3; + }; + CEE1667A2DD5F6F20012CF61 = { + CreatedOnToolsVersion = 16.3; + }; CEE6BE6E2D7AE7FD00E4048B = { CreatedOnToolsVersion = 16.2; }; @@ -296,6 +432,8 @@ targets = ( 97C146ED1CF9000F007C117D /* Runner */, CEE6BE6E2D7AE7FD00E4048B /* EnteMemoryWidgetExtension */, + CEE166222DD5E7820012CF61 /* EnteAlbumWidgetExtension */, + CEE1667A2DD5F6F20012CF61 /* EntePeopleWidgetExtension */, ); }; /* End PBXProject section */ @@ -314,6 +452,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CEE166212DD5E7820012CF61 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CEE166792DD5F6F20012CF61 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; CEE6BE6D2D7AE7FD00E4048B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -389,6 +541,7 @@ "${BUILT_PRODUCTS_DIR}/flutter_native_splash/flutter_native_splash.framework", "${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework", "${BUILT_PRODUCTS_DIR}/flutter_sodium/flutter_sodium.framework", + "${BUILT_PRODUCTS_DIR}/flutter_timezone/flutter_timezone.framework", "${BUILT_PRODUCTS_DIR}/fluttertoast/fluttertoast.framework", "${BUILT_PRODUCTS_DIR}/home_widget/home_widget.framework", "${BUILT_PRODUCTS_DIR}/image_editor_common/image_editor_common.framework", @@ -484,6 +637,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_native_splash.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_sodium.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_timezone.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fluttertoast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/home_widget.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_editor_common.framework", @@ -609,6 +763,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CEE1661F2DD5E7820012CF61 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CEE166772DD5F6F20012CF61 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; CEE6BE6B2D7AE7FD00E4048B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -619,6 +787,16 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + CEE1662F2DD5E7830012CF61 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = CEE166222DD5E7820012CF61 /* EnteAlbumWidgetExtension */; + targetProxy = CEE1662E2DD5E7830012CF61 /* PBXContainerItemProxy */; + }; + CEE166872DD5F6F30012CF61 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = CEE1667A2DD5F6F20012CF61 /* EntePeopleWidgetExtension */; + targetProxy = CEE166862DD5F6F30012CF61 /* PBXContainerItemProxy */; + }; CEE6BE7B2D7AE7FE00E4048B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = CEE6BE6E2D7AE7FD00E4048B /* EnteMemoryWidgetExtension */; @@ -951,6 +1129,250 @@ }; name = Release; }; + CEE166312DD5E7830012CF61 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = EnteAlbumWidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6Z68YJY9Q2; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = EnteAlbumWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = EnteAlbumWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.frame.debug.EnteAlbumWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + CEE166322DD5E7830012CF61 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = EnteAlbumWidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6Z68YJY9Q2; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = EnteAlbumWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = EnteAlbumWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.frame.EnteAlbumWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + CEE166332DD5E7830012CF61 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = EnteAlbumWidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6Z68YJY9Q2; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = EnteAlbumWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = EnteAlbumWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.frame.EnteAlbumWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; + CEE166892DD5F6F30012CF61 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = EntePeopleWidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6Z68YJY9Q2; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = EntePeopleWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = EntePeopleWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.frame.debug.EntePeopleWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + CEE1668A2DD5F6F30012CF61 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = EntePeopleWidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6Z68YJY9Q2; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = EntePeopleWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = EntePeopleWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.frame.EntePeopleWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + CEE1668B2DD5F6F30012CF61 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = EntePeopleWidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6Z68YJY9Q2; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = EntePeopleWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = EntePeopleWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.frame.EntePeopleWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; CEE6BE7D2D7AE7FE00E4048B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1096,6 +1518,26 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + CEE166352DD5E7830012CF61 /* Build configuration list for PBXNativeTarget "EnteAlbumWidgetExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CEE166312DD5E7830012CF61 /* Debug */, + CEE166322DD5E7830012CF61 /* Release */, + CEE166332DD5E7830012CF61 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CEE1668D2DD5F6F30012CF61 /* Build configuration list for PBXNativeTarget "EntePeopleWidgetExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CEE166892DD5F6F30012CF61 /* Debug */, + CEE1668A2DD5F6F30012CF61 /* Release */, + CEE1668B2DD5F6F30012CF61 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; CEE6BE812D7AE7FE00E4048B /* Build configuration list for PBXNativeTarget "EnteMemoryWidgetExtension" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/mobile/lib/app.dart b/mobile/lib/app.dart index 3b8e3346d8..a01071654e 100644 --- a/mobile/lib/app.dart +++ b/mobile/lib/app.dart @@ -13,12 +13,14 @@ import 'package:media_extension/media_extension_action_types.dart'; import "package:photos/core/event_bus.dart"; import 'package:photos/ente_theme_data.dart'; import "package:photos/events/memories_changed_event.dart"; +import "package:photos/events/people_changed_event.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/service_locator.dart"; import 'package:photos/services/app_lifecycle_service.dart'; import "package:photos/services/home_widget_service.dart"; import "package:photos/services/memory_home_widget_service.dart"; +import "package:photos/services/people_home_widget_service.dart"; import 'package:photos/services/sync/sync_service.dart'; import 'package:photos/ui/tabs/home_widget.dart'; import "package:photos/ui/viewer/actions/file_viewer.dart"; @@ -51,6 +53,7 @@ class _EnteAppState extends State with WidgetsBindingObserver { final _logger = Logger("EnteAppState"); late Locale? locale; late StreamSubscription _memoriesChangedSubscription; + late StreamSubscription _peopleChangedSubscription; @override void initState() { @@ -69,6 +72,11 @@ class _EnteAppState extends State with WidgetsBindingObserver { await MemoryHomeWidgetService.instance.memoryChanged(); }, ); + _peopleChangedSubscription = Bus.instance.on().listen( + (event) async { + await PeopleHomeWidgetService.instance.peopleChanged(); + }, + ); } @override @@ -168,6 +176,7 @@ class _EnteAppState extends State with WidgetsBindingObserver { void dispose() { WidgetsBinding.instance.removeObserver(this); _memoriesChangedSubscription.cancel(); + _peopleChangedSubscription.cancel(); super.dispose(); } diff --git a/mobile/lib/core/constants.dart b/mobile/lib/core/constants.dart index 78f3db6bad..6215ebc7d7 100644 --- a/mobile/lib/core/constants.dart +++ b/mobile/lib/core/constants.dart @@ -71,7 +71,7 @@ const kSearchSectionLimit = 9; const maxPickAssetLimit = 50; -const iOSGroupID = "group.io.ente.frame.EnteMemoryWidget"; +const iOSGroupIDMemory = "group.io.ente.frame.EnteMemoryWidget"; const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB' 'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ' @@ -101,12 +101,9 @@ const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB' 'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' 'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD/9k='; -const localFileServer = - String.fromEnvironment("localFileServer", defaultValue: ""); - const uploadTempFilePrefix = "upload_file_"; final tempDirCleanUpInterval = kDebugMode - ? const Duration(seconds: 30).inMicroseconds + ? const Duration(hours: 1).inMicroseconds : const Duration(hours: 6).inMicroseconds; const kFilterChipHeight = 32.0; diff --git a/mobile/lib/db/files_db.dart b/mobile/lib/db/files_db.dart index ae702d92b4..1d46bff519 100644 --- a/mobile/lib/db/files_db.dart +++ b/mobile/lib/db/files_db.dart @@ -640,7 +640,6 @@ class FilesDB with SqlDbBase { int visibility = visibleVisibility, DBFilterOptions? filterOptions, bool applyOwnerCheck = false, - bool ignoreSharedFiles = false, }) async { final stopWatch = EnteWatch('getAllPendingOrUploadedFiles')..start(); final order = (asc ?? false ? 'ASC' : 'DESC'); @@ -663,7 +662,7 @@ class FilesDB with SqlDbBase { subQueries.add(' AND $columnMMdVisibility = ?'); args.add(visibility); - if (ignoreSharedFiles == true) { + if (filterOptions?.ignoreSharedItems ?? false) { subQueries.add(' AND $columnOwnerID = ?'); args.add(ownerID); } @@ -696,7 +695,6 @@ class FilesDB with SqlDbBase { int ownerID, { int? limit, bool? asc, - bool ignoreSharedFiles = false, required DBFilterOptions filterOptions, }) async { final db = await instance.sqliteAsyncDB; @@ -708,7 +706,7 @@ class FilesDB with SqlDbBase { 'SELECT * FROM $filesTable WHERE $columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)' ' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))'); - if (ignoreSharedFiles == true) { + if (filterOptions.ignoreSharedItems) { subQueries.add(' AND $columnOwnerID = ?'); args.add(ownerID); } @@ -1685,8 +1683,8 @@ class FilesDB with SqlDbBase { AND $columnUploadedFileID != -1 AND $columnOwnerID = $userID AND $columnLocalID IS NOT NULL - AND ($columnFileSize IS NULL OR $columnFileSize <= 524288000) - AND ($columnDuration IS NULL OR $columnDuration <= 60) + AND ($columnFileSize IS NOT NULL AND $columnFileSize <= 524288000) + AND ($columnDuration IS NOT NULL AND ($columnDuration <= 60 AND $columnDuration > 0)) ORDER BY $columnCreationTime DESC ''', [getInt(fileType), beginDate.microsecondsSinceEpoch], diff --git a/mobile/lib/db/ml/db.dart b/mobile/lib/db/ml/db.dart index cc598da25c..98871b60d4 100644 --- a/mobile/lib/db/ml/db.dart +++ b/mobile/lib/db/ml/db.dart @@ -38,6 +38,8 @@ import 'package:sqlite_async/sqlite_async.dart'; /// /// [clipTable] - Stores the embeddings of the CLIP model /// [fileDataTable] - Stores data about the files that are already processed by the ML models +/// +/// [faceCacheTable] - Stores a all the mappings from personID or clusterID to the faceID that has been used as cover face. class MLDataDB with SqlDbBase implements IMLDataDB { static final Logger _logger = Logger("MLDataDB"); @@ -60,6 +62,7 @@ class MLDataDB with SqlDbBase implements IMLDataDB { fcClusterIDIndex, createClipEmbeddingsTable, createFileDataTable, + createFaceCacheTable, ]; // only have a single app-wide reference to the database @@ -476,6 +479,22 @@ class MLDataDB with SqlDbBase implements IMLDataDB { return maps.map((e) => e[faceIDColumn] as String).toSet(); } + Future> getFaceIDsForClusterOrderedByScore( + String clusterID, { + int limit = 10, + }) async { + final db = await instance.asyncDB; + final faceIdsResult = await db.getAll( + 'SELECT $facesTable.$faceIDColumn FROM $facesTable ' + 'JOIN $faceClustersTable ON $facesTable.$faceIDColumn = $faceClustersTable.$faceIDColumn ' + 'WHERE $faceClustersTable.$clusterIDColumn = ? ' + 'ORDER BY $facesTable.$faceScore DESC ' + 'LIMIT ?', + [clusterID, limit], + ); + return faceIdsResult.map((e) => e[faceIDColumn] as String).toList(); + } + // Get Map of personID to Map of clusterID to faceIDs @override Future>>> @@ -546,6 +565,23 @@ class MLDataDB with SqlDbBase implements IMLDataDB { return faceIdsResult.map((e) => e[faceIDColumn] as String).toSet(); } + Future> getFaceIDsForPersonOrderedByScore( + String personID, { + int limit = 10, + }) async { + final db = await instance.asyncDB; + final faceIdsResult = await db.getAll( + 'SELECT $facesTable.$faceIDColumn FROM $facesTable ' + 'JOIN $faceClustersTable ON $facesTable.$faceIDColumn = $faceClustersTable.$faceIDColumn ' + 'JOIN $clusterPersonTable ON $faceClustersTable.$clusterIDColumn = $clusterPersonTable.$clusterIDColumn ' + 'WHERE $clusterPersonTable.$personIdColumn = ? ' + 'ORDER BY $facesTable.$faceScore DESC ' + 'LIMIT ?', + [personID, limit], + ); + return faceIdsResult.map((e) => e[faceIDColumn] as String).toList(); + } + @override Future> getBlurValuesForCluster(String clusterID) async { final db = await instance.asyncDB; @@ -1388,4 +1424,49 @@ class MLDataDB with SqlDbBase implements IMLDataDB { embedding.version, ]; } + + /// WARNING: Better to use the similarly named [putFaceIdCachedForPersonOrCluster] + /// method from face_thumbnail_cache instead! + Future putFaceIdCachedForPersonOrCluster( + String personOrClusterId, + String faceID, + ) async { + final db = await instance.asyncDB; + await db.execute( + ''' + INSERT OR REPLACE INTO $faceCacheTable ($personOrClusterIdColumn, $faceIDColumn) + VALUES (?, ?) + ''', + [personOrClusterId, faceID], + ); + } + + Future getFaceIdUsedForPersonOrCluster( + String personOrClusterId, + ) async { + final db = await instance.asyncDB; + final List> maps = await db.getAll( + ''' + SELECT $faceIDColumn FROM $faceCacheTable + WHERE $personOrClusterIdColumn = ? + ''', + [personOrClusterId], + ); + if (maps.isNotEmpty) { + return maps.first[faceIDColumn] as String; + } + return null; + } + + Future removeFaceIdCachedForPersonOrCluster( + String personOrClusterID, + ) async { + final db = await instance.asyncDB; + const String sql = ''' + DELETE FROM $faceCacheTable + WHERE $personOrClusterIdColumn = ?' + '''; + final List params = [personOrClusterID]; + await db.execute(sql, params); + } } diff --git a/mobile/lib/db/ml/schema.dart b/mobile/lib/db/ml/schema.dart index 5968215cf5..d2306cd5d2 100644 --- a/mobile/lib/db/ml/schema.dart +++ b/mobile/lib/db/ml/schema.dart @@ -15,6 +15,7 @@ const imageHeight = 'height'; const mlVersionColumn = 'ml_version'; const personIdColumn = 'person_id'; const clusterIDColumn = 'cluster_id'; +const personOrClusterIdColumn = 'person_or_cluster_id'; const createFacesTable = '''CREATE TABLE IF NOT EXISTS $facesTable ( $fileIDColumn INTEGER NOT NULL, @@ -123,3 +124,16 @@ CREATE TABLE IF NOT EXISTS $fileDataTable ( '''; const deleteFileDataTable = 'DELETE FROM $fileDataTable'; + +// ## FACE CACHE TABLE +const faceCacheTable = 'face_cache'; + +const createFaceCacheTable = ''' +CREATE TABLE IF NOT EXISTS $faceCacheTable ( + $personOrClusterIdColumn TEXT NOT NULL UNIQUE, + $faceIDColumn TEXT NOT NULL UNIQUE, + PRIMARY KEY ($personOrClusterIdColumn) +); +'''; + +const deleteFaceCacheTable = 'DELETE FROM $faceCacheTable'; diff --git a/mobile/lib/events/album_sort_order_change_event.dart b/mobile/lib/events/album_sort_order_change_event.dart new file mode 100644 index 0000000000..29c40477f1 --- /dev/null +++ b/mobile/lib/events/album_sort_order_change_event.dart @@ -0,0 +1,3 @@ +import "package:photos/events/event.dart"; + +class AlbumSortOrderChangeEvent extends Event {} diff --git a/mobile/lib/events/clear_album_selections_event.dart b/mobile/lib/events/clear_album_selections_event.dart new file mode 100644 index 0000000000..1d7ddb7011 --- /dev/null +++ b/mobile/lib/events/clear_album_selections_event.dart @@ -0,0 +1,3 @@ +import "package:photos/events/event.dart"; + +class ClearAlbumSelectionsEvent extends Event {} diff --git a/mobile/lib/events/preview_updated_event.dart b/mobile/lib/events/preview_updated_event.dart deleted file mode 100644 index 8e9cb65aaa..0000000000 --- a/mobile/lib/events/preview_updated_event.dart +++ /dev/null @@ -1,10 +0,0 @@ -import "dart:collection"; - -import "package:photos/events/event.dart"; -import "package:photos/models/preview/preview_item.dart"; - -class PreviewUpdatedEvent extends Event { - final LinkedHashMap items; - - PreviewUpdatedEvent(this.items); -} diff --git a/mobile/lib/extensions/user_extension.dart b/mobile/lib/extensions/user_extension.dart index 20a36030b3..5204847724 100644 --- a/mobile/lib/extensions/user_extension.dart +++ b/mobile/lib/extensions/user_extension.dart @@ -12,4 +12,12 @@ extension UserExtension on User { String? get linkedPersonID => PersonService.instance.emailToPartialPersonDataMapCache[email] ?[PersonService.kPersonIDKey]; + + String get nameOrEmail { + if (PersonService.isInitialized) { + return displayName ?? email.substring(0, email.indexOf("@")); + } else { + return email.substring(0, email.indexOf("@")); + } + } } diff --git a/mobile/lib/generated/intl/messages_ar.dart b/mobile/lib/generated/intl/messages_ar.dart index 9550bdd5e1..796e8e9cfd 100644 --- a/mobile/lib/generated/intl/messages_ar.dart +++ b/mobile/lib/generated/intl/messages_ar.dart @@ -97,220 +97,224 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} ملفات، ${formattedSize} لكل منها"; - static String m27(newEmail) => "تم تغيير البريد الإلكتروني إلى ${newEmail}"; + static String m27(name) => "هذا البريد الإلكتروني مرتبط مسبقاً بـ ${name}."; - static String m28(email) => "${email} لا يملك حساب Ente."; + static String m28(newEmail) => "تم تغيير البريد الإلكتروني إلى ${newEmail}"; - static String m29(email) => + static String m29(email) => "${email} لا يملك حساب Ente."; + + static String m30(email) => "${email} لا يملك حسابًا على Ente.\n\nأرسل له دعوة لمشاركة الصور."; - static String m30(name) => "معانقة ${name}"; + static String m31(name) => "معانقة ${name}"; - static String m31(text) => "تم العثور على صور إضافية لـ ${text}"; + static String m32(text) => "تم العثور على صور إضافية لـ ${text}"; - static String m32(name) => "الاستمتاع بالطعام مع ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: 'ملف واحد', two: 'ملفان', few: '${formattedNumber} ملفات', many: '${formattedNumber} ملفًا', other: '${formattedNumber} ملفًا')} على هذا الجهاز تم نسخه احتياطيًا بأمان"; + static String m33(name) => "الاستمتاع بالطعام مع ${name}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: 'ملف واحد', two: 'ملفان', few: '${formattedNumber} ملفات', many: '${formattedNumber} ملفًا', other: '${formattedNumber} ملفًا')} على هذا الجهاز تم نسخه احتياطيًا بأمان"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: 'ملف واحد', two: 'ملفان', few: '${formattedNumber} ملفات', many: '${formattedNumber} ملفًا', other: '${formattedNumber} ملفًا')} في هذا الألبوم تم نسخه احتياطيًا بأمان"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} جيجابايت مجانية في كل مرة يشترك فيها شخص بخطة مدفوعة ويطبق رمزك"; - static String m36(endDate) => "التجربة المجانية صالحة حتى ${endDate}"; + static String m37(endDate) => "التجربة المجانية صالحة حتى ${endDate}"; - static String m37(count) => + static String m38(count) => "لا يزال بإمكانك الوصول ${Intl.plural(count, one: 'إليه', two: 'إليهما', other: 'إليها')} على Ente طالما لديك اشتراك نشط."; - static String m38(sizeInMBorGB) => "تحرير ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "تحرير ${sizeInMBorGB}"; - static String m39(count, formattedSize) => + static String m40(count, formattedSize) => "${Intl.plural(count, one: 'يمكن حذفه من الجهاز لتحرير ${formattedSize}', two: 'يمكن حذفهما من الجهاز لتحرير ${formattedSize}', few: 'يمكن حذفها من الجهاز لتحرير ${formattedSize}', many: 'يمكن حذفها من الجهاز لتحرير ${formattedSize}', other: 'يمكن حذفها من الجهاز لتحرير ${formattedSize}')}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "جارٍ المعالجة ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "التنزه مع ${name}"; + static String m42(name) => "التنزه مع ${name}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} عُنْصُر', other: '${count} عَنَاصِر')}"; - static String m43(name) => "آخر مرة مع ${name}"; + static String m44(name) => "آخر مرة مع ${name}"; - static String m44(email) => "${email} دعاك لتكون جهة اتصال موثوقة"; + static String m45(email) => "${email} دعاك لتكون جهة اتصال موثوقة"; - static String m45(expiryTime) => "ستنتهي صلاحية الرابط في ${expiryTime}"; + static String m46(expiryTime) => "ستنتهي صلاحية الرابط في ${expiryTime}"; - static String m46(email) => "ربط الشخص بـ ${email}"; + static String m47(email) => "ربط الشخص بـ ${email}"; - static String m47(personName, email) => + static String m48(personName, email) => "سيؤدي هذا إلى ربط ${personName} بـ ${email}"; - static String m48(count, formattedCount) => + static String m49(count, formattedCount) => "${Intl.plural(count, zero: 'لا توجد ذكريات', one: 'ذكرى واحدة', two: 'ذكريتان', few: '${formattedCount} ذكريات', many: '${formattedCount} ذكرى', other: '${formattedCount} ذكرى')}"; - static String m49(count) => + static String m50(count) => "${Intl.plural(count, one: 'نقل عنصر', two: 'نقل عنصرين', few: 'نقل ${count} عناصر', many: 'نقل ${count} عنصرًا', other: 'نقل ${count} عنصرًا')}"; - static String m50(albumName) => "تم النقل بنجاح إلى ${albumName}"; + static String m51(albumName) => "تم النقل بنجاح إلى ${albumName}"; - static String m51(personName) => "لا توجد اقتراحات لـ ${personName}"; + static String m52(personName) => "لا توجد اقتراحات لـ ${personName}"; - static String m52(name) => "ليس ${name}؟"; + static String m53(name) => "ليس ${name}؟"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "يرجى الاتصال بـ ${familyAdminEmail} لتغيير الرمز الخاص بك."; - static String m54(name) => "الاحتفال مع ${name}"; + static String m55(name) => "الاحتفال مع ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "قوة كلمة المرور: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "يرجى التواصل مع دعم ${providerName} إذا تم خصم المبلغ منك."; - static String m57(name, age) => "${name} يبلغ ${age}!"; + static String m58(name, age) => "${name} يبلغ ${age}!"; - static String m58(name, age) => "${name} سيبلغ ${age} قريبًا"; - - static String m59(count) => - "${Intl.plural(count, zero: 'لا توجد صور', one: 'صورة واحدة', two: 'صورتان', few: '${count} صور', many: '${count} صورة', other: '${count} صورة')}"; + static String m59(name, age) => "${name} سيبلغ ${age} قريبًا"; static String m60(count) => "${Intl.plural(count, zero: 'لا توجد صور', one: 'صورة واحدة', two: 'صورتان', few: '${count} صور', many: '${count} صورة', other: '${count} صورة')}"; - static String m61(endDate) => + static String m61(count) => + "${Intl.plural(count, zero: 'لا توجد صور', one: 'صورة واحدة', two: 'صورتان', few: '${count} صور', many: '${count} صورة', other: '${count} صورة')}"; + + static String m62(endDate) => "التجربة المجانية صالحة حتى ${endDate}.\nيمكنك اختيار خطة مدفوعة بعد ذلك."; - static String m62(toEmail) => + static String m63(toEmail) => "يرجى مراسلتنا عبر البريد الإلكتروني على ${toEmail}"; - static String m63(toEmail) => "يرجى إرسال السجلات إلى \n${toEmail}"; + static String m64(toEmail) => "يرجى إرسال السجلات إلى \n${toEmail}"; - static String m64(name) => "التقاط صور مع ${name}"; + static String m65(name) => "التقاط صور مع ${name}"; - static String m65(folderName) => "جارٍ معالجة ${folderName}..."; + static String m66(folderName) => "جارٍ معالجة ${folderName}..."; - static String m66(storeName) => "قيّمنا على ${storeName}"; + static String m67(storeName) => "قيّمنا على ${storeName}"; - static String m67(name) => "تمت إعادة تعيينك إلى ${name}"; + static String m68(name) => "تمت إعادة تعيينك إلى ${name}"; - static String m68(days, email) => + static String m69(days, email) => "يمكنك الوصول إلى الحساب بعد ${days} أيام. سيتم إرسال إشعار إلى ${email}."; - static String m69(email) => + static String m70(email) => "يمكنك الآن استرداد حساب ${email} عن طريق تعيين كلمة مرور جديدة."; - static String m70(email) => "${email} يحاول استرداد حسابك."; + static String m71(email) => "${email} يحاول استرداد حسابك."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. تحصلون كلاكما على ${storageInGB} جيجابايت* مجانًا"; - static String m72(userEmail) => + static String m73(userEmail) => "سيتم إزالة ${userEmail} من هذا الألبوم المشترك.\n\nسيتم أيضًا إزالة أي صور أضافها إلى الألبوم."; - static String m73(endDate) => "يتجدد الاشتراك في ${endDate}"; + static String m74(endDate) => "يتجدد الاشتراك في ${endDate}"; - static String m74(name) => "رحلة برية مع ${name}"; + static String m75(name) => "رحلة برية مع ${name}"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} النتائج التي تم العثور عليها', other: '${count} النتائج التي تم العثور عليها')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "عدم تطابق طول الأقسام: ${snapshotLength} != ${searchLength}"; - static String m77(count) => "تم تحديد ${count}"; + static String m78(count) => "تم تحديد ${count}"; - static String m78(count, yourCount) => + static String m79(count, yourCount) => "تم تحديد ${count} (${yourCount} منها لك)"; - static String m79(name) => "صور سيلفي مع ${name}"; - - static String m80(verificationID) => - "إليك معرّف التحقق الخاص بي لـ ente.io: ${verificationID}"; + static String m80(name) => "صور سيلفي مع ${name}"; static String m81(verificationID) => + "إليك معرّف التحقق الخاص بي لـ ente.io: ${verificationID}"; + + static String m82(verificationID) => "مرحبًا، هل يمكنك تأكيد أن هذا هو معرّف التحقق الخاص بك على ente.io: ${verificationID}؟"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "رمز إحالة Ente الخاص بي: ${referralCode}\n\nطبقه في الإعدادات ← عام ← الإحالات للحصول على ${referralStorageInGB} جيجابايت مجانًا بعد الاشتراك في خطة مدفوعة.\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'مشاركة مع أشخاص محددين', one: 'تمت المشاركة مع شخص واحد', two: 'تمت المشاركة مع شخصين', few: 'تمت المشاركة مع ${numberOfPeople} أشخاص', many: 'تمت المشاركة مع ${numberOfPeople} شخصًا', other: 'تمت المشاركة مع ${numberOfPeople} شخصًا')}"; - static String m84(emailIDs) => "تمت المشاركة مع ${emailIDs}"; + static String m85(emailIDs) => "تمت المشاركة مع ${emailIDs}"; - static String m85(fileType) => "سيتم حذف ${fileType} من جهازك."; + static String m86(fileType) => "سيتم حذف ${fileType} من جهازك."; - static String m86(fileType) => "${fileType} موجود في Ente وعلى جهازك."; + static String m87(fileType) => "${fileType} موجود في Ente وعلى جهازك."; - static String m87(fileType) => "سيتم حذف ${fileType} من Ente."; + static String m88(fileType) => "سيتم حذف ${fileType} من Ente."; - static String m88(name) => "الرياضة مع ${name}"; + static String m89(name) => "الرياضة مع ${name}"; - static String m89(name) => "تسليط الضوء على ${name}"; + static String m90(name) => "تسليط الضوء على ${name}"; - static String m90(storageAmountInGB) => "${storageAmountInGB} جيجابايت"; + static String m91(storageAmountInGB) => "${storageAmountInGB} جيجابايت"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "تم استخدام ${usedAmount} ${usedStorageUnit} من ${totalAmount} ${totalStorageUnit}"; - static String m92(id) => + static String m93(id) => "تم ربط ${id} الخاص بك بحساب Ente آخر.\nإذا كنت ترغب في استخدام ${id} مع هذا الحساب، يرجى الاتصال بدعمنا."; - static String m93(endDate) => "سيتم إلغاء اشتراكك في ${endDate}"; + static String m94(endDate) => "سيتم إلغاء اشتراكك في ${endDate}"; - static String m94(completed, total) => "${completed}/${total} ذكريات محفوظة"; + static String m95(completed, total) => "${completed}/${total} ذكريات محفوظة"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "انقر للتحميل، تم تجاهل التحميل حاليًا بسبب ${ignoreReason}"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "سيحصلون أيضًا على ${storageAmountInGB} جيجابايت"; - static String m97(email) => "هذا هو معرّف التحقق الخاص بـ ${email}"; + static String m98(email) => "هذا هو معرّف التحقق الخاص بـ ${email}"; - static String m98(count) => + static String m99(count) => "${Intl.plural(count, one: 'هذا الأسبوع، قبل سنة', two: 'هذا الأسبوع، قبل سنتين', few: 'هذا الأسبوع، قبل ${count} سنوات', many: 'هذا الأسبوع، قبل ${count} سنة', other: 'هذا الأسبوع، قبل ${count} سنة')}"; - static String m99(dateFormat) => "${dateFormat} عبر السنين"; + static String m100(dateFormat) => "${dateFormat} عبر السنين"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: 'قريبًا', one: 'يوم واحد', two: 'يومان', few: '${count} أيام', many: '${count} يومًا', other: '${count} يومًا')}"; - static String m101(year) => "رحلة في ${year}"; + static String m102(year) => "رحلة في ${year}"; - static String m102(location) => "رحلة إلى ${location}"; + static String m103(location) => "رحلة إلى ${location}"; - static String m103(email) => + static String m104(email) => "لقد تمت دعوتك لتكون جهة اتصال موثوقة بواسطة ${email}."; - static String m104(galleryType) => + static String m105(galleryType) => "نوع المعرض ${galleryType} غير مدعوم لإعادة التسمية."; - static String m105(ignoreReason) => "تم تجاهل التحميل بسبب ${ignoreReason}"; + static String m106(ignoreReason) => "تم تجاهل التحميل بسبب ${ignoreReason}"; - static String m106(count) => "جارٍ حفظ ${count} ذكريات..."; + static String m107(count) => "جارٍ حفظ ${count} ذكريات..."; - static String m107(endDate) => "صالح حتى ${endDate}"; + static String m108(endDate) => "صالح حتى ${endDate}"; - static String m108(email) => "التحقق من ${email}"; + static String m109(email) => "التحقق من ${email}"; - static String m109(count) => - "${Intl.plural(count, zero: 'تمت إضافة 0 مشاهدين', one: 'تمت إضافة مشاهد واحد', two: 'تمت إضافة مشاهدين', few: 'تمت إضافة ${count} مشاهدين', many: 'تمت إضافة ${count} مشاهدًا', other: 'تمت إضافة ${count} مشاهدًا')}"; - - static String m110(email) => - "لقد أرسلنا بريدًا إلكترونيًا إلى ${email}"; + static String m110(name) => "عرض ${name} لإلغاء الربط"; static String m111(count) => + "${Intl.plural(count, zero: 'تمت إضافة 0 مشاهدين', one: 'تمت إضافة مشاهد واحد', two: 'تمت إضافة مشاهدين', few: 'تمت إضافة ${count} مشاهدين', many: 'تمت إضافة ${count} مشاهدًا', other: 'تمت إضافة ${count} مشاهدًا')}"; + + static String m112(email) => + "لقد أرسلنا بريدًا إلكترونيًا إلى ${email}"; + + static String m113(count) => "${Intl.plural(count, one: 'قبل سنة', two: 'قبل سنتين', few: 'قبل ${count} سنوات', many: 'قبل ${count} سنة', other: 'قبل ${count} سنة')}"; - static String m112(name) => "أنت و ${name}"; + static String m114(name) => "أنت و ${name}"; - static String m113(storageSaved) => "لقد حررت ${storageSaved} بنجاح!"; + static String m115(storageSaved) => "لقد حررت ${storageSaved} بنجاح!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -324,7 +328,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("الحساب تم تكوينه بالفعل."), "accountOwnerPersonAppbarTitle": m0, "accountWelcomeBack": - MessageLookupByLibrary.simpleMessage("مرحبًا مجددًا!"), + MessageLookupByLibrary.simpleMessage("أهلاً بعودتك!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( "أدرك أنني إذا فقدت كلمة المرور، فقد أفقد بياناتي لأنها مشفرة بالكامل من طرف إلى طرف."), "activeSessions": @@ -532,24 +536,6 @@ class MessageLookup extends MessageLookupByLibrary { "blackFridaySale": MessageLookupByLibrary.simpleMessage("تخفيضات الجمعة السوداء"), "blog": MessageLookupByLibrary.simpleMessage("المدونة"), - "cLBulkEdit": - MessageLookupByLibrary.simpleMessage("تعديل التواريخ بشكل جماعي"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "يمكنك الآن تحديد صور متعددة، وتعديل التاريخ/الوقت لجميعها بإجراء سريع واحد. تغيير التواريخ مدعوم أيضًا."), - "cLFamilyPlan": - MessageLookupByLibrary.simpleMessage("حدود الخطة العائلية"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "يمكنك الآن تعيين حدود لمقدار التخزين الذي يمكن لأفراد عائلتك استخدامه."), - "cLIcon": MessageLookupByLibrary.simpleMessage("أيقونة جديدة"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "أخيرًا، أيقونة تطبيق جديدة، نعتقد أنها تمثل عملنا على أفضل وجه. أضفنا أيضًا مبدل أيقونات حتى تتمكن من الاستمرار في استخدام الأيقونة القديمة."), - "cLMemories": MessageLookupByLibrary.simpleMessage("الذكريات"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "أعد اكتشاف لحظاتك الخاصة - تسليط الضوء على الأشخاص المفضلين لديك، رحلاتك وعطلاتك، أفضل لقطاتك، وأكثر من ذلك بكثير. قم بتشغيل تعلم الآلة، ضع علامة على نفسك وقم بتسمية أصدقائك للحصول على أفضل تجربة."), - "cLWidgets": - MessageLookupByLibrary.simpleMessage("الأدوات المصغرة (Widgets)"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "الأدوات المصغرة للشاشة الرئيسية المدمجة مع الذكريات متاحة الآن. ستعرض لحظاتك الخاصة دون فتح التطبيق."), "cachedData": MessageLookupByLibrary.simpleMessage("البيانات المؤقتة"), "calculating": MessageLookupByLibrary.simpleMessage("جارٍ الحساب..."), "canNotOpenBody": MessageLookupByLibrary.simpleMessage( @@ -848,6 +834,7 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m25, "duplicateItemsGroup": m26, "edit": MessageLookupByLibrary.simpleMessage("تعديل"), + "editEmailAlreadyLinked": m27, "editLocation": MessageLookupByLibrary.simpleMessage("تعديل الموقع"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("تعديل الموقع"), @@ -860,17 +847,17 @@ class MessageLookup extends MessageLookupByLibrary { "eligible": MessageLookupByLibrary.simpleMessage("مؤهل"), "email": MessageLookupByLibrary.simpleMessage("البريد الإلكتروني"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( - "البريد الإلكتروني مُسَجَّل من قبل."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "البريد الإلكتروني مُسجل من قبل."), + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("البريد الإلكتروني غير مسجل."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage( "تأكيد عنوان البريد الإلكتروني"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( "إرسال سجلاتك عبر البريد الإلكتروني"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("جهات اتصال الطوارئ"), "empty": MessageLookupByLibrary.simpleMessage("إفراغ"), @@ -946,7 +933,7 @@ class MessageLookup extends MessageLookupByLibrary { "exportYourData": MessageLookupByLibrary.simpleMessage("تصدير بياناتك"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("تم العثور على صور إضافية"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "لم يتم تجميع الوجه بعد، يرجى العودة لاحقًا"), "faceRecognition": @@ -981,7 +968,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("الأسئلة الشائعة"), "faqs": MessageLookupByLibrary.simpleMessage("الأسئلة الشائعة"), "favorite": MessageLookupByLibrary.simpleMessage("المفضلة"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("ملاحظات"), "file": MessageLookupByLibrary.simpleMessage("ملف"), "fileFailedToSaveToGallery": @@ -995,8 +982,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("أنواع الملفات"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("أنواع وأسماء الملفات"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("تم حذف الملفات."), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("تم حفظ الملفات في المعرض."), @@ -1013,26 +1000,26 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("الوجوه التي تم العثور عليها"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "تم المطالبة بمساحة التخزين المجانية"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "مساحة تخزين مجانية متاحة للاستخدام"), "freeTrial": MessageLookupByLibrary.simpleMessage("تجربة مجانية"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("تحرير مساحة على الجهاز"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "وفر مساحة على جهازك عن طريق مسح الملفات التي تم نسخها احتياطيًا."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("تحرير المساحة"), - "freeUpSpaceSaving": m39, + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("المعرض"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "يتم عرض ما يصل إلى 1000 ذكرى في المعرض."), "general": MessageLookupByLibrary.simpleMessage("عام"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "جارٍ إنشاء مفاتيح التشفير..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("الانتقال إلى الإعدادات"), "googlePlayId": @@ -1061,7 +1048,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "إخفاء العناصر المشتركة من معرض الصفحة الرئيسية"), "hiding": MessageLookupByLibrary.simpleMessage("جارٍ الإخفاء..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("مستضاف في OSM France"), "howItWorks": MessageLookupByLibrary.simpleMessage("كيف يعمل"), @@ -1116,7 +1103,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "يبدو أن خطأً ما قد حدث. يرجى المحاولة مرة أخرى بعد بعض الوقت. إذا استمر الخطأ، يرجى الاتصال بفريق الدعم لدينا."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "تعرض العناصر عدد الأيام المتبقية قبل الحذف الدائم."), @@ -1138,7 +1125,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "يرجى مساعدتنا بهذه المعلومات"), "language": MessageLookupByLibrary.simpleMessage("اللغة"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("آخر تحديث"), "lastYearsTrip": MessageLookupByLibrary.simpleMessage("رحلة العام الماضي"), @@ -1152,7 +1139,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("جهات الاتصال الموثوقة"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("الحسابات الموثوقة"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "تسمح جهات الاتصال الموثوقة لأشخاص معينين بالوصول إلى حسابك في حالة غيابك."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1169,7 +1156,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("لمشاركة أسرع"), "linkEnabled": MessageLookupByLibrary.simpleMessage("مفعّل"), "linkExpired": MessageLookupByLibrary.simpleMessage("منتهي الصلاحية"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("انتهاء صلاحية الرابط"), "linkHasExpired": @@ -1178,11 +1165,13 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("ربط الشخص"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage("لتجربة مشاركة أفضل"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("الصور الحية"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "يمكنك مشاركة اشتراكك مع عائلتك."), + "loadMessage2": MessageLookupByLibrary.simpleMessage( + "لقد حفظنا أكثر من 200 مليون ذكرى حتى الآن."), "loadMessage3": MessageLookupByLibrary.simpleMessage( "نحتفظ بـ 3 نسخ من بياناتك، إحداها في ملجأ للطوارئ تحت الأرض."), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1266,7 +1255,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("أنا"), - "memoryCount": m48, + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("المنتجات الترويجية"), "mergeWithExisting": @@ -1298,13 +1287,13 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("الأحدث"), "mostRelevant": MessageLookupByLibrary.simpleMessage("الأكثر صلة"), "mountains": MessageLookupByLibrary.simpleMessage("فوق التلال"), - "moveItem": m49, + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "نقل الصور المحددة إلى تاريخ واحد"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("نقل إلى ألبوم"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("نقل إلى الألبوم المخفي"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("تم النقل إلى سلة المهملات"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1359,10 +1348,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("لا توجد نتائج"), "noResultsFound": MessageLookupByLibrary.simpleMessage("لم يتم العثور على نتائج."), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("لم يتم العثور على قفل نظام."), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("ليس هذا الشخص؟"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "لم تتم مشاركة أي شيء معك بعد"), @@ -1375,7 +1364,7 @@ class MessageLookup extends MessageLookupByLibrary { "على Ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("على الطريق مرة أخرى"), - "onlyFamilyAdminCanChangeCode": m53, + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("هم فقط"), "oops": MessageLookupByLibrary.simpleMessage("عفوًا"), "oopsCouldNotSaveEdits": @@ -1405,7 +1394,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("اكتمل الإقران"), "panorama": MessageLookupByLibrary.simpleMessage("بانوراما"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage("التحقق لا يزال معلقًا."), "passkey": MessageLookupByLibrary.simpleMessage("مفتاح المرور"), @@ -1413,19 +1402,19 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("التحقق من مفتاح المرور"), "password": MessageLookupByLibrary.simpleMessage("كلمة المرور"), "passwordChangedSuccessfully": - MessageLookupByLibrary.simpleMessage("تم تغيير كلمة المرور بنجاح."), + MessageLookupByLibrary.simpleMessage("تم تغيير كلمة المرور بنجاح"), "passwordLock": MessageLookupByLibrary.simpleMessage("قفل بكلمة مرور"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "يتم حساب قوة كلمة المرور مع الأخذ في الاعتبار طول كلمة المرور، والأحرف المستخدمة، وما إذا كانت كلمة المرور تظهر في قائمة أفضل 10,000 كلمة مرور شائعة الاستخدام."), "passwordWarning": MessageLookupByLibrary.simpleMessage( - "نحن لا نخزن كلمة المرور هذه، لذا إذا نسيتها، لا يمكننا المساعدة في فك تشفير بياناتك."), + "نحن لا نقوم بتخزين كلمة المرور هذه، لذا إذا نسيتها، لا يمكننا فك تشفير بياناتك"), "paymentDetails": MessageLookupByLibrary.simpleMessage("تفاصيل الدفع"), "paymentFailed": MessageLookupByLibrary.simpleMessage("فشلت عملية الدفع"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "للأسف، فشلت عملية الدفع الخاصة بك. يرجى الاتصال بالدعم وسوف نساعدك!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("العناصر المعلقة"), "pendingSync": MessageLookupByLibrary.simpleMessage("المزامنة المعلقة"), "people": MessageLookupByLibrary.simpleMessage("الأشخاص"), @@ -1436,20 +1425,20 @@ class MessageLookup extends MessageLookupByLibrary { "permanentlyDelete": MessageLookupByLibrary.simpleMessage("حذف نهائي"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage("حذف نهائي من الجهاز؟"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("اسم الشخص"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("رفاق فروي"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("أوصاف الصور"), "photoGridSize": MessageLookupByLibrary.simpleMessage("حجم شبكة الصور"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("صورة"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("الصور"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "ستتم إزالة الصور التي أضفتها من الألبوم."), - "photosCount": m60, + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "تحتفظ الصور بالفرق الزمني النسبي"), @@ -1460,7 +1449,7 @@ class MessageLookup extends MessageLookupByLibrary { "playOnTv": MessageLookupByLibrary.simpleMessage("تشغيل الألبوم على التلفزيون"), "playOriginal": MessageLookupByLibrary.simpleMessage("تشغيل الأصلي"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("تشغيل البث"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("اشتراك متجر Play"), @@ -1473,14 +1462,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "يرجى الاتصال بالدعم إذا استمرت المشكلة."), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("يرجى منح الأذونات."), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("يرجى تسجيل الدخول مرة أخرى."), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "يرجى تحديد الروابط السريعة للإزالة."), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("يرجى المحاولة مرة أخرى"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1494,7 +1483,7 @@ class MessageLookup extends MessageLookupByLibrary { "يرجى الانتظار لبعض الوقت قبل إعادة المحاولة."), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "يرجى الانتظار، قد يستغرق هذا بعض الوقت."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("جارٍ تحضير السجلات..."), "preserveMore": MessageLookupByLibrary.simpleMessage("حفظ المزيد"), @@ -1512,7 +1501,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("متابعة"), "processed": MessageLookupByLibrary.simpleMessage("تمت المعالجة"), "processing": MessageLookupByLibrary.simpleMessage("المعالجة"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("معالجة مقاطع الفيديو"), "publicLinkCreated": @@ -1525,10 +1514,10 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("فتح تذكرة دعم"), "rateTheApp": MessageLookupByLibrary.simpleMessage("تقييم التطبيق"), "rateUs": MessageLookupByLibrary.simpleMessage("تقييم التطبيق"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("إعادة تعيين \"أنا\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("جارٍ إعادة التعيين..."), "recover": MessageLookupByLibrary.simpleMessage("استعادة"), @@ -1539,7 +1528,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("استرداد الحساب"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("بدء الاسترداد"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("مفتاح الاسترداد"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "تم نسخ مفتاح الاسترداد إلى الحافظة"), @@ -1553,12 +1542,12 @@ class MessageLookup extends MessageLookupByLibrary { "تم التحقق من مفتاح الاسترداد."), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "مفتاح الاسترداد هو الطريقة الوحيدة لاستعادة صورك إذا نسيت كلمة المرور. يمكنك العثور عليه في الإعدادات > الحساب.\n\nالرجاء إدخال مفتاح الاسترداد هنا للتحقق من أنك حفظته بشكل صحيح."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("تم الاسترداد بنجاح!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "جهة اتصال موثوقة تحاول الوصول إلى حسابك"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "لا يمكن التحقق من كلمة المرور على جهازك الحالي، لكن يمكننا تعديلها لتعمل على جميع الأجهزة.\n\nسجّل الدخول باستخدام مفتاح الاسترداد، ثم أنشئ كلمة مرور جديدة (يمكنك اختيار نفس الكلمة السابقة إذا أردت)."), "recreatePasswordTitle": @@ -1574,7 +1563,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("1. أعطِ هذا الرمز لأصدقائك"), "referralStep2": MessageLookupByLibrary.simpleMessage("2. يشتركون في خطة مدفوعة"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("الإحالات"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("الإحالات متوقفة مؤقتًا"), @@ -1603,7 +1592,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("إزالة الرابط"), "removeParticipant": MessageLookupByLibrary.simpleMessage("إزالة المشارك"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("إزالة تسمية الشخص"), "removePublicLink": @@ -1624,7 +1613,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("إعادة تسمية الملف"), "renewSubscription": MessageLookupByLibrary.simpleMessage("تجديد الاشتراك"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("الإبلاغ عن خطأ"), "reportBug": MessageLookupByLibrary.simpleMessage("الإبلاغ عن خطأ"), "resendEmail": MessageLookupByLibrary.simpleMessage( @@ -1650,7 +1639,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("مراجعة الاقتراحات"), "right": MessageLookupByLibrary.simpleMessage("يمين"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("تدوير"), "rotateLeft": MessageLookupByLibrary.simpleMessage("تدوير لليسار"), "rotateRight": MessageLookupByLibrary.simpleMessage("تدوير لليمين"), @@ -1704,8 +1693,8 @@ class MessageLookup extends MessageLookupByLibrary { "ادعُ الأشخاص، وسترى جميع الصور التي شاركوها هنا."), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "سيتم عرض الأشخاص هنا بمجرد اكتمال المعالجة والمزامنة."), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("الأمان"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "رؤية روابط الألبومات العامة في التطبيق"), @@ -1750,9 +1739,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "سيتم إزالة العناصر المحددة من هذا الشخص، ولكن لن يتم حذفها من مكتبتك."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, - "selfiesWithThem": m79, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("إرسال"), "sendEmail": MessageLookupByLibrary.simpleMessage("إرسال بريد إلكتروني"), @@ -1782,16 +1771,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("شارك ألبومًا الآن"), "shareLink": MessageLookupByLibrary.simpleMessage("مشاركة الرابط"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "شارك فقط مع الأشخاص الذين تريدهم."), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "قم بتنزيل تطبيق Ente حتى نتمكن من مشاركة الصور ومقاطع الفيديو بالجودة الأصلية بسهولة.\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "المشاركة مع غير مستخدمي Ente"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("شارك ألبومك الأول"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1804,7 +1793,7 @@ class MessageLookup extends MessageLookupByLibrary { "إشعارات الصور المشتركة الجديدة"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "تلقّ إشعارات عندما يضيف شخص ما صورة إلى ألبوم مشترك أنت جزء منه."), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("تمت مشاركتها معي"), "sharedWithYou": @@ -1822,11 +1811,11 @@ class MessageLookup extends MessageLookupByLibrary { "تسجيل الخروج من الأجهزة الأخرى"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "أوافق على شروط الخدمة وسياسة الخصوصية"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "سيتم حذفه من جميع الألبومات."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("تخط"), "social": MessageLookupByLibrary.simpleMessage("التواصل الاجتماعي"), "someItemsAreInBothEnteAndYourDevice": @@ -1844,6 +1833,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "حدث خطأ ما، يرجى المحاولة مرة أخرى"), "sorry": MessageLookupByLibrary.simpleMessage("عفوًا"), + "sorryBackupFailedDesc": MessageLookupByLibrary.simpleMessage( + "عذرًا، لم نتمكن من عمل نسخة احتياطية لهذا الملف الآن، سنعيد المحاولة لاحقًا."), "sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage( "عذرًا، تعذرت الإضافة إلى المفضلة!"), "sorryCouldNotRemoveFromFavorites": @@ -1860,8 +1851,8 @@ class MessageLookup extends MessageLookupByLibrary { "sortNewestFirst": MessageLookupByLibrary.simpleMessage("الأحدث أولاً"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("الأقدم أولاً"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ نجاح"), - "sportsWithThem": m88, - "spotlightOnThem": m89, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("تسليط الضوء عليك"), "startAccountRecoveryTitle": @@ -1875,14 +1866,14 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("التخزين"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("العائلة"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("أنت"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("تم تجاوز حد التخزين."), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("تفاصيل البث"), "strongStrength": MessageLookupByLibrary.simpleMessage("قوية"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("اشتراك"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "المشاركة متاحة فقط للاشتراكات المدفوعة النشطة."), @@ -1899,7 +1890,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("اقتراح ميزة"), "sunrise": MessageLookupByLibrary.simpleMessage("على الأفق"), "support": MessageLookupByLibrary.simpleMessage("الدعم"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("توقفت المزامنة"), "syncing": MessageLookupByLibrary.simpleMessage("جارٍ المزامنة..."), "systemTheme": MessageLookupByLibrary.simpleMessage("النظام"), @@ -1908,7 +1899,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("انقر لإدخال الرمز"), "tapToUnlock": MessageLookupByLibrary.simpleMessage("انقر لفتح القفل"), "tapToUpload": MessageLookupByLibrary.simpleMessage("انقر للتحميل"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "يبدو أن خطأً ما قد حدث. يرجى المحاولة مرة أخرى بعد بعض الوقت. إذا استمر الخطأ، يرجى الاتصال بفريق الدعم لدينا."), "terminate": MessageLookupByLibrary.simpleMessage("إنهاء"), @@ -1932,7 +1923,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "سيتم حذف هذه العناصر من جهازك."), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "سيتم حذفها من جميع الألبومات."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1949,12 +1940,12 @@ class MessageLookup extends MessageLookupByLibrary { "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "لا تحتوي هذه الصورة على بيانات EXIF."), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("هذا أنا!"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "هذا هو معرّف التحقق الخاص بك"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("هذا الأسبوع عبر السنين"), - "thisWeekXYearsAgo": m98, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "سيؤدي هذا إلى تسجيل خروجك من الجهاز التالي:"), @@ -1966,7 +1957,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "سيؤدي هذا إلى إزالة الروابط العامة لجميع الروابط السريعة المحددة."), - "throughTheYears": m99, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "لتمكين قفل التطبيق، يرجى إعداد رمز مرور الجهاز أو قفل الشاشة في إعدادات النظام."), @@ -1980,13 +1971,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("المجموع"), "totalSize": MessageLookupByLibrary.simpleMessage("الحجم الإجمالي"), "trash": MessageLookupByLibrary.simpleMessage("سلة المهملات"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("قص"), - "tripInYear": m101, - "tripToLocation": m102, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("جهات الاتصال الموثوقة"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("المحاولة مرة أخرى"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "قم بتشغيل النسخ الاحتياطي لتحميل الملفات المضافة إلى مجلد الجهاز هذا تلقائيًا إلى Ente."), @@ -2003,7 +1994,7 @@ class MessageLookup extends MessageLookupByLibrary { "تمت إعادة تعيين المصادقة الثنائية بنجاح."), "twofactorSetup": MessageLookupByLibrary.simpleMessage("إعداد المصادقة الثنائية"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("إلغاء الأرشفة"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("إلغاء أرشفة الألبوم"), @@ -2027,10 +2018,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("جارٍ تحديث تحديد المجلد..."), "upgrade": MessageLookupByLibrary.simpleMessage("ترقية"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "جارٍ تحميل الملفات إلى الألبوم..."), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("جارٍ حفظ ذكرى واحدة..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -2048,7 +2039,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("استخدام الصورة المحددة"), "usedSpace": MessageLookupByLibrary.simpleMessage("المساحة المستخدمة"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "فشل التحقق، يرجى المحاولة مرة أخرى."), @@ -2056,7 +2047,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("التحقق"), "verifyEmail": MessageLookupByLibrary.simpleMessage("التحقق من البريد الإلكتروني"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("تحقق"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("التحقق من مفتاح المرور"), @@ -2067,7 +2058,8 @@ class MessageLookup extends MessageLookupByLibrary { "جارٍ التحقق من مفتاح الاسترداد..."), "videoInfo": MessageLookupByLibrary.simpleMessage("معلومات الفيديو"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("فيديو"), - "videoStreaming": MessageLookupByLibrary.simpleMessage("بث الفيديو"), + "videoStreaming": + MessageLookupByLibrary.simpleMessage("مقاطع فيديو قابلة للبث"), "videos": MessageLookupByLibrary.simpleMessage("مقاطع الفيديو"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("عرض الجلسات النشطة"), @@ -2080,10 +2072,11 @@ class MessageLookup extends MessageLookupByLibrary { "viewLargeFilesDesc": MessageLookupByLibrary.simpleMessage( "عرض الملفات التي تستهلك أكبر قدر من مساحة التخزين."), "viewLogs": MessageLookupByLibrary.simpleMessage("عرض السجلات"), + "viewPersonToUnlink": m110, "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("عرض مفتاح الاسترداد"), "viewer": MessageLookupByLibrary.simpleMessage("مشاهد"), - "viewersSuccessfullyAdded": m109, + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "يرجى زيارة web.ente.io لإدارة اشتراكك."), "waitingForVerification": @@ -2096,7 +2089,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "لا ندعم تعديل الصور والألبومات التي لا تملكها بعد."), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("ضعيفة"), "welcomeBack": MessageLookupByLibrary.simpleMessage("أهلاً بعودتك!"), "whatsNew": MessageLookupByLibrary.simpleMessage("ما الجديد"), @@ -2104,7 +2097,7 @@ class MessageLookup extends MessageLookupByLibrary { "يمكن لجهة الاتصال الموثوقة المساعدة في استعادة بياناتك."), "yearShort": MessageLookupByLibrary.simpleMessage("سنة"), "yearly": MessageLookupByLibrary.simpleMessage("سنويًا"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("نعم"), "yesCancel": MessageLookupByLibrary.simpleMessage("نعم، إلغاء"), "yesConvertToViewer": @@ -2118,7 +2111,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage("نعم، إعادة تعيين الشخص"), "you": MessageLookupByLibrary.simpleMessage("أنت"), - "youAndThem": m112, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("أنت مشترك في خطة عائلية!"), "youAreOnTheLatestVersion": @@ -2137,9 +2130,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("لا يمكنك المشاركة مع نفسك."), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "لا توجد لديك أي عناصر مؤرشفة."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": - MessageLookupByLibrary.simpleMessage("تم حذف حسابك بنجاح."), + MessageLookupByLibrary.simpleMessage("تم حذف حسابك بنجاح"), "yourMap": MessageLookupByLibrary.simpleMessage("خريطتك"), "yourPlanWasSuccessfullyDowngraded": MessageLookupByLibrary.simpleMessage("تم تخفيض خطتك بنجاح."), diff --git a/mobile/lib/generated/intl/messages_be.dart b/mobile/lib/generated/intl/messages_be.dart index c671f70439..805a9d9b72 100644 --- a/mobile/lib/generated/intl/messages_be.dart +++ b/mobile/lib/generated/intl/messages_be.dart @@ -20,12 +20,12 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'be'; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Надзейнасць пароля: ${passwordStrengthValue}"; - static String m90(storageAmountInGB) => "${storageAmountInGB} Гб"; + static String m91(storageAmountInGB) => "${storageAmountInGB} Гб"; - static String m110(email) => + static String m112(email) => "Ліст адпраўлены на электронную пошту ${email}"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -187,7 +187,7 @@ class MessageLookup extends MessageLookupByLibrary { "password": MessageLookupByLibrary.simpleMessage("Пароль"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("Пароль паспяхова зменены"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordWarning": MessageLookupByLibrary.simpleMessage( "Мы не захоўваем гэты пароль і мы не зможам расшыфраваць вашы даныя, калі вы забудзеце яго"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("фота"), @@ -249,7 +249,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Немагчыма згенерыраваць ключы бяспекі на гэтай прыладзе.\n\nЗарэгіструйцеся з іншай прылады."), "status": MessageLookupByLibrary.simpleMessage("Стан"), - "storageInGB": m90, + "storageInGB": m91, "strongStrength": MessageLookupByLibrary.simpleMessage("Надзейны"), "support": MessageLookupByLibrary.simpleMessage("Падтрымка"), "systemTheme": MessageLookupByLibrary.simpleMessage("Сістэма"), @@ -288,7 +288,7 @@ class MessageLookup extends MessageLookupByLibrary { "videoSmallCase": MessageLookupByLibrary.simpleMessage("відэа"), "viewLargeFiles": MessageLookupByLibrary.simpleMessage("Вялікія файлы"), "viewer": MessageLookupByLibrary.simpleMessage("Праглядальнік"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Ненадзейны"), "welcomeBack": MessageLookupByLibrary.simpleMessage("З вяртаннем!"), "yesDelete": MessageLookupByLibrary.simpleMessage("Так, выдаліць"), diff --git a/mobile/lib/generated/intl/messages_cs.dart b/mobile/lib/generated/intl/messages_cs.dart index da24cd9874..5e2be5e926 100644 --- a/mobile/lib/generated/intl/messages_cs.dart +++ b/mobile/lib/generated/intl/messages_cs.dart @@ -22,17 +22,17 @@ class MessageLookup extends MessageLookupByLibrary { static String m6(albumName) => "Úspěšně přidáno do ${albumName}"; - static String m45(expiryTime) => "Platnost odkazu vyprší ${expiryTime}"; + static String m46(expiryTime) => "Platnost odkazu vyprší ${expiryTime}"; - static String m66(storeName) => "Ohodnoťte nás na ${storeName}"; + static String m67(storeName) => "Ohodnoťte nás na ${storeName}"; - static String m73(endDate) => "Předplatné se obnoví ${endDate}"; + static String m74(endDate) => "Předplatné se obnoví ${endDate}"; - static String m79(name) => "Selfie s ${name}"; + static String m80(name) => "Selfie s ${name}"; - static String m108(email) => "Ověřit ${email}"; + static String m109(email) => "Ověřit ${email}"; - static String m112(name) => "Vy a ${name}"; + static String m114(name) => "Vy a ${name}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -91,9 +91,6 @@ class MessageLookup extends MessageLookupByLibrary { "backupStatus": MessageLookupByLibrary.simpleMessage("Stav zálohování"), "birthday": MessageLookupByLibrary.simpleMessage("Narozeniny"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLIcon": MessageLookupByLibrary.simpleMessage("Nová ikona"), - "cLMemories": MessageLookupByLibrary.simpleMessage("Vzpomínky"), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widgety"), "cachedData": MessageLookupByLibrary.simpleMessage("Data uložená v mezipaměti"), "calculating": @@ -292,7 +289,7 @@ class MessageLookup extends MessageLookupByLibrary { "leaveAlbum": MessageLookupByLibrary.simpleMessage("Opustit album"), "left": MessageLookupByLibrary.simpleMessage("Doleva"), "lightTheme": MessageLookupByLibrary.simpleMessage("Světlý"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkHasExpired": MessageLookupByLibrary.simpleMessage("Platnost odkazu vypršela"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Nikdy"), @@ -388,7 +385,7 @@ class MessageLookup extends MessageLookupByLibrary { "queued": MessageLookupByLibrary.simpleMessage("Ve frontě"), "radius": MessageLookupByLibrary.simpleMessage("Rádius"), "rateUs": MessageLookupByLibrary.simpleMessage("Ohodnoť nás"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "recoverButton": MessageLookupByLibrary.simpleMessage("Obnovit"), "recoveryKeyVerified": MessageLookupByLibrary.simpleMessage("Obnovovací klíč byl ověřen"), @@ -417,7 +414,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Přejmenovat album"), "renameFile": MessageLookupByLibrary.simpleMessage("Přejmenovat soubor"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Nahlásit chybu"), "reportBug": MessageLookupByLibrary.simpleMessage("Nahlásit chybu"), "resendEmail": @@ -459,7 +456,7 @@ class MessageLookup extends MessageLookupByLibrary { "selectTime": MessageLookupByLibrary.simpleMessage("Vybrat čas"), "selectYourPlan": MessageLookupByLibrary.simpleMessage("Vyberte svůj plán"), - "selfiesWithThem": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Odeslat"), "sendEmail": MessageLookupByLibrary.simpleMessage("Odeslat e-mail"), "sendInvite": MessageLookupByLibrary.simpleMessage("Odeslat pozvánku"), @@ -527,7 +524,7 @@ class MessageLookup extends MessageLookupByLibrary { "usedSpace": MessageLookupByLibrary.simpleMessage("Využité místo"), "verify": MessageLookupByLibrary.simpleMessage("Ověřit"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Ověřit e-mail"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Ověřit"), "verifying": MessageLookupByLibrary.simpleMessage("Ověřování..."), "verifyingRecoveryKey": MessageLookupByLibrary.simpleMessage( @@ -551,7 +548,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesRemove": MessageLookupByLibrary.simpleMessage("Ano, odstranit"), "yesRenew": MessageLookupByLibrary.simpleMessage("Ano, obnovit"), "you": MessageLookupByLibrary.simpleMessage("Vy"), - "youAndThem": m112, + "youAndThem": m114, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Váš účet byl smazán"), "yourMap": MessageLookupByLibrary.simpleMessage("Vaše mapa") diff --git a/mobile/lib/generated/intl/messages_da.dart b/mobile/lib/generated/intl/messages_da.dart index da5f4ee32e..5eb3c19630 100644 --- a/mobile/lib/generated/intl/messages_da.dart +++ b/mobile/lib/generated/intl/messages_da.dart @@ -29,24 +29,24 @@ class MessageLookup extends MessageLookupByLibrary { static String m24(supportEmail) => "Send venligst en email til ${supportEmail} fra din registrerede email adresse"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB hver gang nogen tilmelder sig et betalt abonnement og anvender din kode"; - static String m45(expiryTime) => "Link udløber den ${expiryTime}"; + static String m46(expiryTime) => "Link udløber den ${expiryTime}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Kodeordets styrke: ${passwordStrengthValue}"; - static String m77(count) => "${count} valgt"; + static String m78(count) => "${count} valgt"; - static String m81(verificationID) => + static String m82(verificationID) => "Hey, kan du bekræfte, at dette er dit ente.io verifikation ID: ${verificationID}"; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m96(storageAmountInGB) => "De får også ${storageAmountInGB} GB"; + static String m97(storageAmountInGB) => "De får også ${storageAmountInGB} GB"; - static String m110(email) => + static String m112(email) => "Vi har sendt en email til ${email}"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -219,7 +219,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Find folk hurtigt ved navn"), "forgotPassword": MessageLookupByLibrary.simpleMessage("Glemt adgangskode"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Frigør enhedsplads"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -253,7 +253,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Enheds grænse"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktiveret"), "linkExpired": MessageLookupByLibrary.simpleMessage("Udløbet"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Udløb af link"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Linket er udløbet"), @@ -303,7 +303,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Adgangskoden er blevet ændret"), "passwordLock": MessageLookupByLibrary.simpleMessage("Adgangskodelås"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordWarning": MessageLookupByLibrary.simpleMessage( "Vi gemmer ikke denne adgangskode, så hvis du glemmer den kan vi ikke dekryptere dine data"), "pendingItems": @@ -375,7 +375,7 @@ class MessageLookup extends MessageLookupByLibrary { "selectedFoldersWillBeEncryptedAndBackedUp": MessageLookupByLibrary.simpleMessage( "Valgte mapper vil blive krypteret og sikkerhedskopieret"), - "selectedPhotos": m77, + "selectedPhotos": m78, "sendEmail": MessageLookupByLibrary.simpleMessage("Send email"), "sendLink": MessageLookupByLibrary.simpleMessage("Send link"), "setPasswordTitle": @@ -383,7 +383,7 @@ class MessageLookup extends MessageLookupByLibrary { "setupComplete": MessageLookupByLibrary.simpleMessage("Opsætning fuldført"), "shareALink": MessageLookupByLibrary.simpleMessage("Del et link"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage("Del med ikke Ente brugere"), "showMemories": MessageLookupByLibrary.simpleMessage("Vis minder"), @@ -403,7 +403,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Beklager, vi kunne ikke generere sikre krypteringsnøgler på denne enhed.\n\nForsøg venligst at oprette en konto fra en anden enhed."), "status": MessageLookupByLibrary.simpleMessage("Status"), - "storageInGB": m90, + "storageInGB": m91, "strongStrength": MessageLookupByLibrary.simpleMessage("Stærkt"), "subscribe": MessageLookupByLibrary.simpleMessage("Abonner"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( @@ -417,7 +417,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Afslut session?"), "termsOfServicesTitle": MessageLookupByLibrary.simpleMessage("Betingelser"), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "thisCanBeUsedToRecoverYourAccountIfYou": MessageLookupByLibrary.simpleMessage( "Dette kan bruges til at gendanne din konto, hvis du mister din anden faktor"), @@ -456,7 +456,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewer": MessageLookupByLibrary.simpleMessage("Seer"), "waitingForWifi": MessageLookupByLibrary.simpleMessage("Venter på Wi-fi..."), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Svagt"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Velkommen tilbage!"), diff --git a/mobile/lib/generated/intl/messages_de.dart b/mobile/lib/generated/intl/messages_de.dart index af0429846a..6e4f1a8a5e 100644 --- a/mobile/lib/generated/intl/messages_de.dart +++ b/mobile/lib/generated/intl/messages_de.dart @@ -84,6 +84,9 @@ class MessageLookup extends MessageLookupByLibrary { static String m21(count) => "${Intl.plural(count, one: 'Lösche ${count} Element', other: 'Lösche ${count} Elemente')}"; + static String m116(count) => + "Sollen die Fotos (und Videos) aus diesen ${count} Alben auch aus allen anderen Alben gelöscht werden, in denen sie enthalten sind?"; + static String m22(currentlyDeleting, totalCount) => "Lösche ${currentlyDeleting} / ${totalCount}"; @@ -99,225 +102,231 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} Dateien, ${formattedSize} jede"; - static String m27(newEmail) => "E-Mail-Adresse geändert zu ${newEmail}"; + static String m27(name) => "Diese E-Mail ist bereits verknüpft mit ${name}."; - static String m28(email) => "${email} hat kein Ente-Konto."; + static String m28(newEmail) => "E-Mail-Adresse geändert zu ${newEmail}"; - static String m29(email) => + static String m29(email) => "${email} hat kein Ente-Konto."; + + static String m30(email) => "${email} hat kein Ente-Konto.\n\nSende eine Einladung, um Fotos zu teilen."; - static String m30(name) => "${name} umarmen"; + static String m31(name) => "${name} umarmen"; - static String m31(text) => "Zusätzliche Fotos für ${text} gefunden"; + static String m32(text) => "Zusätzliche Fotos für ${text} gefunden"; - static String m32(name) => "Feiern mit ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 Datei', other: '${formattedNumber} Dateien')} auf diesem Gerät wurde(n) sicher gespeichert"; + static String m33(name) => "Feiern mit ${name}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 Datei', other: '${formattedNumber} Dateien')} auf diesem Gerät wurde(n) sicher gespeichert"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 Datei', other: '${formattedNumber} Dateien')} in diesem Album wurde(n) sicher gespeichert"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB jedes Mal, wenn sich jemand mit deinem Code für einen bezahlten Tarif anmeldet"; - static String m36(endDate) => "Kostenlose Demo verfügbar bis zum ${endDate}"; + static String m37(endDate) => "Kostenlose Demo verfügbar bis zum ${endDate}"; - static String m37(count) => + static String m38(count) => "Du hast ${Intl.plural(count, one: 'darauf', other: 'auf sie')} weiterhin Zugriff, solange du ein aktives Abo hast"; - static String m38(sizeInMBorGB) => "${sizeInMBorGB} freigeben"; + static String m39(sizeInMBorGB) => "${sizeInMBorGB} freigeben"; - static String m39(count, formattedSize) => + static String m40(count, formattedSize) => "${Intl.plural(count, one: 'Es kann vom Gerät gelöscht werden, um ${formattedSize} freizugeben', other: 'Sie können vom Gerät gelöscht werden, um ${formattedSize} freizugeben')}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Verarbeite ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Wandern mit ${name}"; + static String m42(name) => "Wandern mit ${name}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} Objekt', other: '${count} Objekte')}"; - static String m43(name) => "Zuletzt mit ${name}"; + static String m44(name) => "Zuletzt mit ${name}"; - static String m44(email) => + static String m45(email) => "${email} hat dich eingeladen, ein vertrauenswürdiger Kontakt zu werden"; - static String m45(expiryTime) => "Link läuft am ${expiryTime} ab"; + static String m46(expiryTime) => "Link läuft am ${expiryTime} ab"; - static String m46(email) => "Person mit ${email} verknüpfen"; + static String m47(email) => "Person mit ${email} verknüpfen"; - static String m47(personName, email) => + static String m48(personName, email) => "Dies wird ${personName} mit ${email} verknüpfen"; - static String m48(count, formattedCount) => + static String m49(count, formattedCount) => "${Intl.plural(count, zero: 'keine Erinnerungen', one: '${formattedCount} Erinnerung', other: '${formattedCount} Erinnerungen')}"; - static String m49(count) => + static String m50(count) => "${Intl.plural(count, one: 'Element verschieben', other: 'Elemente verschieben')}"; - static String m50(albumName) => "Erfolgreich zu ${albumName} hinzugefügt"; + static String m51(albumName) => "Erfolgreich zu ${albumName} hinzugefügt"; - static String m51(personName) => "Keine Vorschläge für ${personName}"; + static String m52(personName) => "Keine Vorschläge für ${personName}"; - static String m52(name) => "Nicht ${name}?"; + static String m53(name) => "Nicht ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Bitte wende Dich an ${familyAdminEmail}, um den Code zu ändern."; - static String m54(name) => "Party mit ${name}"; + static String m55(name) => "Party mit ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Passwortstärke: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Bitte kontaktiere den Support von ${providerName}, falls etwas abgebucht wurde"; - static String m57(name, age) => "${name} ist ${age}!"; + static String m58(name, age) => "${name} ist ${age}!"; - static String m58(name, age) => "${name} wird bald ${age}"; - - static String m59(count) => - "${Intl.plural(count, zero: 'Keine Fotos', one: 'Ein Foto', other: '${count} Fotos')}"; + static String m59(name, age) => "${name} wird bald ${age}"; static String m60(count) => + "${Intl.plural(count, zero: 'Keine Fotos', one: 'Ein Foto', other: '${count} Fotos')}"; + + static String m61(count) => "${Intl.plural(count, zero: '0 Fotos', one: 'Ein Foto', other: '${count} Fotos')}"; - static String m61(endDate) => + static String m62(endDate) => "Kostenlose Testversion gültig bis ${endDate}.\nDu kannst anschließend ein bezahltes Paket auswählen."; - static String m62(toEmail) => "Bitte sende uns eine E-Mail an ${toEmail}"; + static String m63(toEmail) => "Bitte sende uns eine E-Mail an ${toEmail}"; - static String m63(toEmail) => "Bitte sende die Protokolle an ${toEmail}"; + static String m64(toEmail) => "Bitte sende die Protokolle an ${toEmail}"; - static String m64(name) => "Posieren mit ${name}"; + static String m65(name) => "Posieren mit ${name}"; - static String m65(folderName) => "Verarbeite ${folderName}..."; + static String m66(folderName) => "Verarbeite ${folderName}..."; - static String m66(storeName) => "Bewerte uns auf ${storeName}"; + static String m67(storeName) => "Bewerte uns auf ${storeName}"; - static String m67(name) => "Du wurdest an ${name} neu zugewiesen"; + static String m68(name) => "Du wurdest an ${name} neu zugewiesen"; - static String m68(days, email) => + static String m69(days, email) => "Du kannst nach ${days} Tagen auf das Konto zugreifen. Eine Benachrichtigung wird an ${email} versendet."; - static String m69(email) => + static String m70(email) => "Du kannst jetzt das Konto von ${email} wiederherstellen, indem du ein neues Passwort setzt."; - static String m70(email) => + static String m71(email) => "${email} versucht, dein Konto wiederherzustellen."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Ihr beide erhaltet ${storageInGB} GB* kostenlos"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} wird aus diesem geteilten Album entfernt\n\nAlle von ihnen hinzugefügte Fotos werden ebenfalls aus dem Album entfernt"; - static String m73(endDate) => "Erneuert am ${endDate}"; + static String m74(endDate) => "Erneuert am ${endDate}"; - static String m74(name) => "Roadtrip mit ${name}"; + static String m75(name) => "Roadtrip mit ${name}"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} Ergebnis gefunden', other: '${count} Ergebnisse gefunden')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Abschnittslänge stimmt nicht überein: ${snapshotLength} != ${searchLength}"; - static String m77(count) => "${count} ausgewählt"; + static String m117(count) => "${count} ausgewählt"; - static String m78(count, yourCount) => + static String m78(count) => "${count} ausgewählt"; + + static String m79(count, yourCount) => "${count} ausgewählt (${yourCount} von Ihnen)"; - static String m79(name) => "Selfies mit ${name}"; - - static String m80(verificationID) => - "Hier ist meine Verifizierungs-ID: ${verificationID} für ente.io."; + static String m80(name) => "Selfies mit ${name}"; static String m81(verificationID) => + "Hier ist meine Verifizierungs-ID: ${verificationID} für ente.io."; + + static String m82(verificationID) => "Hey, kannst du bestätigen, dass dies deine ente.io Verifizierungs-ID ist: ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Ente Weiterempfehlungs-Code: ${referralCode} \n\nEinlösen unter Einstellungen → Allgemein → Weiterempfehlungen, um ${referralStorageInGB} GB kostenlos zu erhalten, sobald Sie einen kostenpflichtigen Tarif abgeschlossen haben\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Teile mit bestimmten Personen', one: 'Teilen mit 1 Person', other: 'Teilen mit ${numberOfPeople} Personen')}"; - static String m84(emailIDs) => "Geteilt mit ${emailIDs}"; - - static String m85(fileType) => - "Dieses ${fileType} wird von deinem Gerät gelöscht."; + static String m85(emailIDs) => "Geteilt mit ${emailIDs}"; static String m86(fileType) => + "Dieses ${fileType} wird von deinem Gerät gelöscht."; + + static String m87(fileType) => "Diese Datei ist sowohl in Ente als auch auf deinem Gerät."; - static String m87(fileType) => "Diese Datei wird von Ente gelöscht."; + static String m88(fileType) => "Diese Datei wird von Ente gelöscht."; - static String m88(name) => "Sport mit ${name}"; + static String m89(name) => "Sport mit ${name}"; - static String m89(name) => "Spot auf ${name}"; + static String m90(name) => "Spot auf ${name}"; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} von ${totalAmount} ${totalStorageUnit} verwendet"; - static String m92(id) => + static String m93(id) => "Dein ${id} ist bereits mit einem anderen Ente-Konto verknüpft.\nWenn du deine ${id} mit diesem Konto verwenden möchtest, kontaktiere bitte unseren Support"; - static String m93(endDate) => "Dein Abo endet am ${endDate}"; + static String m94(endDate) => "Dein Abo endet am ${endDate}"; - static String m94(completed, total) => + static String m95(completed, total) => "${completed}/${total} Erinnerungsstücke gesichert"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "Zum Hochladen tippen, Hochladen wird derzeit ignoriert, da ${ignoreReason}"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "Diese erhalten auch ${storageAmountInGB} GB"; - static String m97(email) => "Dies ist ${email}s Verifizierungs-ID"; + static String m98(email) => "Dies ist ${email}s Verifizierungs-ID"; - static String m98(count) => + static String m99(count) => "${Intl.plural(count, one: 'Diese Woche, vor einem Jahr', other: 'Diese Woche, vor ${count} Jahren')}"; - static String m99(dateFormat) => "${dateFormat} über die Jahre"; + static String m100(dateFormat) => "${dateFormat} über die Jahre"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Demnächst', one: '1 Tag', other: '${count} Tage')}"; - static String m101(year) => "Reise in ${year}"; + static String m102(year) => "Reise in ${year}"; - static String m102(location) => "Ausflug nach ${location}"; + static String m103(location) => "Ausflug nach ${location}"; - static String m103(email) => + static String m104(email) => "Du wurdest von ${email} eingeladen, ein Kontakt für das digitale Erbe zu werden."; - static String m104(galleryType) => + static String m105(galleryType) => "Der Galerie-Typ ${galleryType} unterstützt kein Umbenennen"; - static String m105(ignoreReason) => + static String m106(ignoreReason) => "Upload wird aufgrund von ${ignoreReason} ignoriert"; - static String m106(count) => "Sichere ${count} Erinnerungsstücke..."; + static String m107(count) => "Sichere ${count} Erinnerungsstücke..."; - static String m107(endDate) => "Gültig bis ${endDate}"; + static String m108(endDate) => "Gültig bis ${endDate}"; - static String m108(email) => "Verifiziere ${email}"; + static String m109(email) => "Verifiziere ${email}"; - static String m109(count) => - "${Intl.plural(count, zero: '0 Betrachter hinzugefügt', one: 'Einen Betrachter hinzugefügt', other: '${count} Betrachter hinzugefügt')}"; - - static String m110(email) => - "Wir haben eine E-Mail an ${email} gesendet"; + static String m110(name) => "${name} zum Entfernen des Links anzeigen"; static String m111(count) => + "${Intl.plural(count, zero: '0 Betrachter hinzugefügt', one: 'Einen Betrachter hinzugefügt', other: '${count} Betrachter hinzugefügt')}"; + + static String m112(email) => + "Wir haben eine E-Mail an ${email} gesendet"; + + static String m113(count) => "${Intl.plural(count, one: 'vor einem Jahr', other: 'vor ${count} Jahren')}"; - static String m112(name) => "Du und ${name}"; + static String m114(name) => "Du und ${name}"; - static String m113(storageSaved) => + static String m115(storageSaved) => "Du hast ${storageSaved} erfolgreich freigegeben!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -336,6 +345,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Willkommen zurück!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( "Ich verstehe, dass ich meine Daten verlieren kann, wenn ich mein Passwort vergesse, da meine Daten Ende-zu-Ende-verschlüsselt sind."), + "actionNotSupportedOnFavouritesAlbum": + MessageLookupByLibrary.simpleMessage( + "Aktion für das Favoritenalbum nicht unterstützt"), "activeSessions": MessageLookupByLibrary.simpleMessage("Aktive Sitzungen"), "add": MessageLookupByLibrary.simpleMessage("Hinzufügen"), @@ -363,6 +375,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Details der Add-ons"), "addOnValidTill": m3, "addOns": MessageLookupByLibrary.simpleMessage("Add-ons"), + "addParticipants": + MessageLookupByLibrary.simpleMessage("Teilnehmer hinzufügen"), "addPhotos": MessageLookupByLibrary.simpleMessage("Fotos hinzufügen"), "addSelected": MessageLookupByLibrary.simpleMessage("Auswahl hinzufügen"), @@ -550,23 +564,6 @@ class MessageLookup extends MessageLookupByLibrary { "blackFridaySale": MessageLookupByLibrary.simpleMessage("Black-Friday-Aktion"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLBulkEdit": MessageLookupByLibrary.simpleMessage( - "Massenbearbeitung von Datumsangaben"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Du kannst jetzt mehrere Fotos auswählen, und das Datum/Uhrzeit für alle mit einer Aktion ändern. Das Verschieben von Daten wird auch unterstützt."), - "cLFamilyPlan": MessageLookupByLibrary.simpleMessage( - "Obergrenzen für den Familientarif"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Du kannst jetzt festlegen, wie viel Speicherplatz deine Familienmitglieder nutzen können."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Neues Icon"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Endlich ein neues App-Icon, das unserer Meinung nach unser Werk am besten repräsentiert. Zudem ist es möglich, weiterhin das alte App-Icon zu verwenden."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Erinnerungen"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Entdecke Deine besonderen Momente neu – Spot auf Deine liebsten Personen, Deine Reisen und Urlaube, Deine besten Schnappschüsse und vieles mehr. Aktiviere das maschinelle Lernen, tagge Dich selbst und benenne Deine Freunde für die besten Ergebnisse."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widgets"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Homescreen-Widgets mit integrierten Erinnerungen sind nun verfügbar. Sie zeigen dir deine besonderen Momente an, ohne die App zu öffnen."), "cachedData": MessageLookupByLibrary.simpleMessage("Daten im Cache"), "calculating": MessageLookupByLibrary.simpleMessage("Wird berechnet..."), @@ -786,6 +783,7 @@ class MessageLookup extends MessageLookupByLibrary { "deleteItemCount": m21, "deleteLocation": MessageLookupByLibrary.simpleMessage("Standort löschen"), + "deleteMultipleAlbumDialog": m116, "deletePhotos": MessageLookupByLibrary.simpleMessage("Fotos löschen"), "deleteProgress": m22, "deleteReason1": MessageLookupByLibrary.simpleMessage( @@ -873,6 +871,7 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m25, "duplicateItemsGroup": m26, "edit": MessageLookupByLibrary.simpleMessage("Bearbeiten"), + "editEmailAlreadyLinked": m27, "editLocation": MessageLookupByLibrary.simpleMessage("Standort bearbeiten"), "editLocationTagTitle": @@ -888,16 +887,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-Mail"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "E-Mail ist bereits registriert."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("E-Mail nicht registriert."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("E-Mail-Verifizierung"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( "Protokolle per E-Mail senden"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Notfallkontakte"), "empty": MessageLookupByLibrary.simpleMessage("Leeren"), @@ -959,6 +958,8 @@ class MessageLookup extends MessageLookupByLibrary { "Bitte gib eine gültige E-Mail-Adresse ein."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage( "Gib deine E-Mail-Adresse ein"), + "enterYourNewEmailAddress": MessageLookupByLibrary.simpleMessage( + "Gib Deine neue E-Mail-Adresse ein"), "enterYourPassword": MessageLookupByLibrary.simpleMessage("Passwort eingeben"), "enterYourRecoveryKey": MessageLookupByLibrary.simpleMessage( @@ -976,7 +977,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Daten exportieren"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Zusätzliche Fotos gefunden"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Gesicht ist noch nicht gruppiert, bitte komm später zurück"), "faceRecognition": @@ -1014,7 +1015,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("Häufig gestellte Fragen"), "faqs": MessageLookupByLibrary.simpleMessage("FAQs"), "favorite": MessageLookupByLibrary.simpleMessage("Favorit"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Rückmeldung"), "file": MessageLookupByLibrary.simpleMessage("Datei"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -1028,8 +1029,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Dateitypen"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Dateitypen und -namen"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Dateien gelöscht"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( @@ -1048,28 +1049,28 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Gesichter gefunden"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Kostenlos hinzugefügter Speicherplatz"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Freier Speicherplatz nutzbar"), "freeTrial": MessageLookupByLibrary.simpleMessage("Kostenlose Testphase"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Gerätespeicher freiräumen"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Spare Speicherplatz auf deinem Gerät, indem du Dateien löschst, die bereits gesichert wurden."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Speicherplatz freigeben"), - "freeUpSpaceSaving": m39, + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Galerie"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Bis zu 1000 Erinnerungsstücke angezeigt in der Galerie"), "general": MessageLookupByLibrary.simpleMessage("Allgemein"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generierung von Verschlüsselungscodes..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Zu den Einstellungen"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), @@ -1099,7 +1100,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Geteilte Elemente in der Home-Galerie ausblenden"), "hiding": MessageLookupByLibrary.simpleMessage("Verstecken..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("Gehostet bei OSM France"), "howItWorks": @@ -1158,7 +1159,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Etwas ist schiefgelaufen. Bitte versuche es später noch einmal. Sollte der Fehler weiter bestehen, kontaktiere unser Supportteam."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Elemente zeigen die Anzahl der Tage bis zum dauerhaften Löschen an"), @@ -1179,7 +1180,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage("Bitte gib diese Daten ein"), "language": MessageLookupByLibrary.simpleMessage("Sprache"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Zuletzt aktualisiert"), "lastYearsTrip": @@ -1194,7 +1195,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Digitales Erbe"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Digital geerbte Konten"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Das digitale Erbe erlaubt vertrauenswürdigen Kontakten den Zugriff auf dein Konto in deiner Abwesenheit."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1211,7 +1212,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("für schnelleres Teilen"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktiviert"), "linkExpired": MessageLookupByLibrary.simpleMessage("Abgelaufen"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Ablaufdatum des Links"), "linkHasExpired": @@ -1220,8 +1221,8 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Person verknüpfen"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "um besseres Teilen zu ermöglichen"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Live-Fotos"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Du kannst dein Abonnement mit deiner Familie teilen"), @@ -1308,7 +1309,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Ich"), - "memoryCount": m48, + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Merchandise"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage( "Mit vorhandenem zusammenführen"), @@ -1340,14 +1341,14 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Neuste"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Nach Relevanz"), "mountains": MessageLookupByLibrary.simpleMessage("Über den Bergen"), - "moveItem": m49, + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Ausgewählte Fotos auf ein Datum verschieben"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Zum Album verschieben"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Zu verstecktem Album verschieben"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage( "In den Papierkorb verschoben"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1401,10 +1402,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Keine Ergebnisse"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Keine Ergebnisse gefunden"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("Keine Systemsperre gefunden"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("Nicht diese Person?"), "nothingSharedWithYouYet": @@ -1418,7 +1419,8 @@ class MessageLookup extends MessageLookupByLibrary { "onEnte": MessageLookupByLibrary.simpleMessage( "Auf ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("Wieder unterwegs"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("An diesem Tag"), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Nur diese"), "oops": MessageLookupByLibrary.simpleMessage("Hoppla"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1448,7 +1450,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Mit PIN verbinden"), "pairingComplete": MessageLookupByLibrary.simpleMessage("Verbunden"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage( "Verifizierung steht noch aus"), "passkey": MessageLookupByLibrary.simpleMessage("Passkey"), @@ -1458,7 +1460,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Passwort erfolgreich geändert"), "passwordLock": MessageLookupByLibrary.simpleMessage("Passwort Sperre"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Die Berechnung der Stärke des Passworts basiert auf dessen Länge, den verwendeten Zeichen, und ob es in den 10.000 am häufigsten verwendeten Passwörtern vorkommt"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1469,7 +1471,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Zahlung fehlgeschlagen"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Leider ist deine Zahlung fehlgeschlagen. Wende dich an unseren Support und wir helfen dir weiter!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Ausstehende Elemente"), "pendingSync": @@ -1483,21 +1485,21 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Dauerhaft löschen"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "Endgültig vom Gerät löschen?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Name der Person"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Pelzige Begleiter"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Foto Beschreibungen"), "photoGridSize": MessageLookupByLibrary.simpleMessage("Fotorastergröße"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("Foto"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Fotos"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Von dir hinzugefügte Fotos werden vom Album entfernt"), - "photosCount": m60, + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "Fotos behalten relativen Zeitunterschied"), @@ -1509,7 +1511,7 @@ class MessageLookup extends MessageLookupByLibrary { "Album auf dem Fernseher wiedergeben"), "playOriginal": MessageLookupByLibrary.simpleMessage("Original abspielen"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Stream abspielen"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore Abo"), @@ -1522,14 +1524,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Bitte wenden Sie sich an den Support, falls das Problem weiterhin besteht"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Bitte erteile die nötigen Berechtigungen"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Bitte logge dich erneut ein"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Bitte wähle die zu entfernenden schnellen Links"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Bitte versuche es erneut"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1543,7 +1545,7 @@ class MessageLookup extends MessageLookupByLibrary { "Bitte warte kurz, bevor du es erneut versuchst"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Bitte warten, dies wird eine Weile dauern."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage( "Protokolle werden vorbereitet..."), "preserveMore": @@ -1563,7 +1565,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Fortfahren"), "processed": MessageLookupByLibrary.simpleMessage("Verarbeitet"), "processing": MessageLookupByLibrary.simpleMessage("In Bearbeitung"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Verarbeite Videos"), "publicLinkCreated": @@ -1576,10 +1578,10 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Ticket erstellen"), "rateTheApp": MessageLookupByLibrary.simpleMessage("App bewerten"), "rateUs": MessageLookupByLibrary.simpleMessage("Bewerte uns"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("\"Ich\" neu zuweisen"), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Ordne neu zu..."), "recover": MessageLookupByLibrary.simpleMessage("Wiederherstellen"), @@ -1591,7 +1593,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Konto wiederherstellen"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Wiederherstellung gestartet"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage( "Wiederherstellungs-Schlüssel"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1606,12 +1608,12 @@ class MessageLookup extends MessageLookupByLibrary { "Wiederherstellungs-Schlüssel überprüft"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Dein Wiederherstellungsschlüssel ist die einzige Möglichkeit, auf deine Fotos zuzugreifen, solltest du dein Passwort vergessen. Du findest ihn unter Einstellungen > Konto.\n\nBitte gib deinen Wiederherstellungsschlüssel hier ein, um sicherzugehen, dass du ihn korrekt gesichert hast."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage( "Wiederherstellung erfolgreich!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Ein vertrauenswürdiger Kontakt versucht, auf dein Konto zuzugreifen"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Das aktuelle Gerät ist nicht leistungsfähig genug, um dein Passwort zu verifizieren, aber wir können es neu erstellen, damit es auf allen Geräten funktioniert.\n\nBitte melde dich mit deinem Wiederherstellungs-Schlüssel an und erstelle dein Passwort neu (Wenn du willst, kannst du dasselbe erneut verwenden)."), "recreatePasswordTitle": @@ -1627,7 +1629,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Gib diesen Code an deine Freunde"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Sie schließen ein bezahltes Abo ab"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Weiterempfehlungen"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Einlösungen sind derzeit pausiert"), @@ -1659,7 +1661,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Link entfernen"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Teilnehmer entfernen"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Personenetikett entfernen"), "removePublicLink": @@ -1679,7 +1681,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Datei umbenennen"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Abonnement erneuern"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Fehler melden"), "reportBug": MessageLookupByLibrary.simpleMessage("Fehler melden"), "resendEmail": @@ -1705,7 +1707,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Vorschläge überprüfen"), "right": MessageLookupByLibrary.simpleMessage("Rechts"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Drehen"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Nach links drehen"), "rotateRight": @@ -1762,8 +1764,8 @@ class MessageLookup extends MessageLookupByLibrary { "Laden Sie Personen ein, damit Sie geteilte Fotos hier einsehen können"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Personen werden hier angezeigt, sobald Verarbeitung und Synchronisierung abgeschlossen sind"), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Sicherheit"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Öffentliche Album-Links in der App ansehen"), @@ -1801,6 +1803,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Wähle dein Gesicht"), "selectYourPlan": MessageLookupByLibrary.simpleMessage("Wähle dein Abo aus"), + "selectedAlbums": m117, "selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage( "Ausgewählte Dateien sind nicht auf Ente"), "selectedFoldersWillBeEncryptedAndBackedUp": @@ -1812,9 +1815,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Ausgewählte Elemente werden von dieser Person entfernt, aber nicht aus deiner Bibliothek gelöscht."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, - "selfiesWithThem": m79, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Absenden"), "sendEmail": MessageLookupByLibrary.simpleMessage("E-Mail senden"), "sendInvite": MessageLookupByLibrary.simpleMessage("Einladung senden"), @@ -1844,16 +1847,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Teile jetzt ein Album"), "shareLink": MessageLookupByLibrary.simpleMessage("Link teilen"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Teile mit ausgewählten Personen"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Hol dir Ente, damit wir ganz einfach Fotos und Videos in Originalqualität teilen können\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Mit Nicht-Ente-Benutzern teilen"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Teile dein erstes Album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1864,7 +1867,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Neue geteilte Fotos"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Erhalte Benachrichtigungen, wenn jemand ein Foto zu einem gemeinsam genutzten Album hinzufügt, dem du angehörst"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Mit mir geteilt"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Mit dir geteilt"), @@ -1882,11 +1885,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Andere Geräte abmelden"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Ich stimme den Nutzungsbedingungen und der Datenschutzerklärung zu"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Es wird aus allen Alben gelöscht."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Überspringen"), "social": MessageLookupByLibrary.simpleMessage("Social Media"), "someItemsAreInBothEnteAndYourDevice": @@ -1925,8 +1928,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Älteste zuerst"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Abgeschlossen"), - "sportsWithThem": m88, - "spotlightOnThem": m89, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Spot auf dich selbst"), "startAccountRecoveryTitle": @@ -1941,14 +1944,14 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Speicherplatz"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Familie"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Sie"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Speichergrenze überschritten"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Stream-Details"), "strongStrength": MessageLookupByLibrary.simpleMessage("Stark"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Abonnieren"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Du benötigst ein aktives, bezahltes Abonnement, um das Teilen zu aktivieren."), @@ -1966,7 +1969,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Verbesserung vorschlagen"), "sunrise": MessageLookupByLibrary.simpleMessage("Am Horizont"), "support": MessageLookupByLibrary.simpleMessage("Support"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronisierung angehalten"), "syncing": MessageLookupByLibrary.simpleMessage("Synchronisiere …"), @@ -1979,7 +1982,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Zum Entsperren antippen"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Zum Hochladen antippen"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Etwas ist schiefgelaufen. Bitte versuche es später noch einmal. Sollte der Fehler weiter bestehen, kontaktiere unser Supportteam."), "terminate": MessageLookupByLibrary.simpleMessage("Beenden"), @@ -2003,7 +2006,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Diese Elemente werden von deinem Gerät gelöscht."), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Sie werden aus allen Alben gelöscht."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -2021,12 +2024,12 @@ class MessageLookup extends MessageLookupByLibrary { "Dieses Bild hat keine Exif-Daten"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Das bin ich!"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Dies ist deine Verifizierungs-ID"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("Diese Woche über die Jahre"), - "thisWeekXYearsAgo": m98, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Dadurch wirst du von folgendem Gerät abgemeldet:"), @@ -2038,7 +2041,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Hiermit werden die öffentlichen Links aller ausgewählten schnellen Links entfernt."), - "throughTheYears": m99, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "Um die App-Sperre zu aktivieren, konfiguriere bitte den Gerätepasscode oder die Bildschirmsperre in den Systemeinstellungen."), @@ -2053,13 +2056,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("Gesamt"), "totalSize": MessageLookupByLibrary.simpleMessage("Gesamtgröße"), "trash": MessageLookupByLibrary.simpleMessage("Papierkorb"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Schneiden"), - "tripInYear": m101, - "tripToLocation": m102, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Vertrauenswürdige Kontakte"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Erneut versuchen"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Aktiviere die Sicherung, um neue Dateien in diesem Ordner automatisch zu Ente hochzuladen."), @@ -2078,7 +2081,7 @@ class MessageLookup extends MessageLookupByLibrary { "Zwei-Faktor-Authentifizierung (2FA) erfolgreich zurückgesetzt"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Zweiten Faktor (2FA) einrichten"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Dearchivieren"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Album dearchivieren"), @@ -2102,10 +2105,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Ordnerauswahl wird aktualisiert..."), "upgrade": MessageLookupByLibrary.simpleMessage("Upgrade"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Dateien werden ins Album hochgeladen..."), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage( "Sichere ein Erinnerungsstück..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -2124,7 +2127,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ausgewähltes Foto verwenden"), "usedSpace": MessageLookupByLibrary.simpleMessage("Belegter Speicherplatz"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verifizierung fehlgeschlagen, bitte versuchen Sie es erneut"), @@ -2133,7 +2136,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Überprüfen"), "verifyEmail": MessageLookupByLibrary.simpleMessage("E-Mail-Adresse verifizieren"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Überprüfen"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Passkey verifizieren"), @@ -2146,7 +2149,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Video-Informationen"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("Video"), "videoStreaming": - MessageLookupByLibrary.simpleMessage("Video-Streaming"), + MessageLookupByLibrary.simpleMessage("Streambare Videos"), "videos": MessageLookupByLibrary.simpleMessage("Videos"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Aktive Sitzungen anzeigen"), @@ -2159,10 +2162,11 @@ class MessageLookup extends MessageLookupByLibrary { "viewLargeFilesDesc": MessageLookupByLibrary.simpleMessage( "Dateien anzeigen, die den meisten Speicherplatz belegen."), "viewLogs": MessageLookupByLibrary.simpleMessage("Protokolle anzeigen"), + "viewPersonToUnlink": m110, "viewRecoveryKey": MessageLookupByLibrary.simpleMessage( "Wiederherstellungsschlüssel anzeigen"), "viewer": MessageLookupByLibrary.simpleMessage("Zuschauer"), - "viewersSuccessfullyAdded": m109, + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Bitte rufe \"web.ente.io\" auf, um dein Abo zu verwalten"), "waitingForVerification": @@ -2175,7 +2179,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Wir unterstützen keine Bearbeitung von Fotos und Alben, die du noch nicht besitzt"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Schwach"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Willkommen zurück!"), @@ -2184,7 +2188,7 @@ class MessageLookup extends MessageLookupByLibrary { "Ein vertrauenswürdiger Kontakt kann helfen, deine Daten wiederherzustellen."), "yearShort": MessageLookupByLibrary.simpleMessage("Jahr"), "yearly": MessageLookupByLibrary.simpleMessage("Jährlich"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Ja"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ja, kündigen"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -2198,7 +2202,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage("Ja, Person zurücksetzen"), "you": MessageLookupByLibrary.simpleMessage("Du"), - "youAndThem": m112, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("Du bist im Familien-Tarif!"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage( @@ -2217,7 +2221,7 @@ class MessageLookup extends MessageLookupByLibrary { "Du kannst nicht mit dir selbst teilen"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Du hast keine archivierten Elemente."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Dein Benutzerkonto wurde gelöscht"), "yourMap": MessageLookupByLibrary.simpleMessage("Deine Karte"), diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart index 89b5981efc..64c7bf315d 100644 --- a/mobile/lib/generated/intl/messages_en.dart +++ b/mobile/lib/generated/intl/messages_en.dart @@ -82,6 +82,9 @@ class MessageLookup extends MessageLookupByLibrary { static String m21(count) => "${Intl.plural(count, one: 'Delete ${count} item', other: 'Delete ${count} items')}"; + static String m116(count) => + "Also delete the photos (and videos) present in these ${count} albums from all other albums they are part of?"; + static String m22(currentlyDeleting, totalCount) => "Deleting ${currentlyDeleting} / ${totalCount}"; @@ -97,224 +100,230 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} files, ${formattedSize} each"; - static String m27(newEmail) => "Email changed to ${newEmail}"; + static String m27(name) => "This email is already linked to ${name}."; - static String m28(email) => "${email} does not have an Ente account."; + static String m28(newEmail) => "Email changed to ${newEmail}"; - static String m29(email) => + static String m29(email) => "${email} does not have an Ente account."; + + static String m30(email) => "${email} does not have an Ente account.\n\nSend them an invite to share photos."; - static String m30(name) => "Embracing ${name}"; + static String m31(name) => "Embracing ${name}"; - static String m31(text) => "Extra photos found for ${text}"; + static String m32(text) => "Extra photos found for ${text}"; - static String m32(name) => "Feasting with ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 file', other: '${formattedNumber} files')} on this device have been backed up safely"; + static String m33(name) => "Feasting with ${name}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 file', other: '${formattedNumber} files')} on this device have been backed up safely"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 file', other: '${formattedNumber} files')} in this album has been backed up safely"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB each time someone signs up for a paid plan and applies your code"; - static String m36(endDate) => "Free trial valid till ${endDate}"; + static String m37(endDate) => "Free trial valid till ${endDate}"; - static String m37(count) => + static String m38(count) => "You can still access ${Intl.plural(count, one: 'it', other: 'them')} on Ente as long as you have an active subscription"; - static String m38(sizeInMBorGB) => "Free up ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Free up ${sizeInMBorGB}"; - static String m39(count, formattedSize) => + static String m40(count, formattedSize) => "${Intl.plural(count, one: 'It can be deleted from the device to free up ${formattedSize}', other: 'They can be deleted from the device to free up ${formattedSize}')}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Processing ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Hiking with ${name}"; + static String m42(name) => "Hiking with ${name}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} item', other: '${count} items')}"; - static String m43(name) => "Last time with ${name}"; + static String m44(name) => "Last time with ${name}"; - static String m44(email) => + static String m45(email) => "${email} has invited you to be a trusted contact"; - static String m45(expiryTime) => "Link will expire on ${expiryTime}"; + static String m46(expiryTime) => "Link will expire on ${expiryTime}"; - static String m46(email) => "Link person to ${email}"; + static String m47(email) => "Link person to ${email}"; - static String m47(personName, email) => + static String m48(personName, email) => "This will link ${personName} to ${email}"; - static String m48(count, formattedCount) => + static String m49(count, formattedCount) => "${Intl.plural(count, zero: 'no memories', one: '${formattedCount} memory', other: '${formattedCount} memories')}"; - static String m49(count) => + static String m50(count) => "${Intl.plural(count, one: 'Move item', other: 'Move items')}"; - static String m50(albumName) => "Moved successfully to ${albumName}"; + static String m51(albumName) => "Moved successfully to ${albumName}"; - static String m51(personName) => "No suggestions for ${personName}"; + static String m52(personName) => "No suggestions for ${personName}"; - static String m52(name) => "Not ${name}?"; + static String m53(name) => "Not ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Please contact ${familyAdminEmail} to change your code."; - static String m54(name) => "Party with ${name}"; + static String m55(name) => "Party with ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Password strength: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Please talk to ${providerName} support if you were charged"; - static String m57(name, age) => "${name} is ${age}!"; + static String m58(name, age) => "${name} is ${age}!"; - static String m58(name, age) => "${name} turning ${age} soon"; - - static String m59(count) => - "${Intl.plural(count, zero: 'No photos', one: '1 photo', other: '${count} photos')}"; + static String m59(name, age) => "${name} turning ${age} soon"; static String m60(count) => + "${Intl.plural(count, zero: 'No photos', one: '1 photo', other: '${count} photos')}"; + + static String m61(count) => "${Intl.plural(count, zero: '0 photos', one: '1 photo', other: '${count} photos')}"; - static String m61(endDate) => + static String m62(endDate) => "Free trial valid till ${endDate}.\nYou can choose a paid plan afterwards."; - static String m62(toEmail) => "Please email us at ${toEmail}"; + static String m63(toEmail) => "Please email us at ${toEmail}"; - static String m63(toEmail) => "Please send the logs to \n${toEmail}"; + static String m64(toEmail) => "Please send the logs to \n${toEmail}"; - static String m64(name) => "Posing with ${name}"; + static String m65(name) => "Posing with ${name}"; - static String m65(folderName) => "Processing ${folderName}..."; + static String m66(folderName) => "Processing ${folderName}..."; - static String m66(storeName) => "Rate us on ${storeName}"; + static String m67(storeName) => "Rate us on ${storeName}"; - static String m67(name) => "Reassigned you to ${name}"; + static String m68(name) => "Reassigned you to ${name}"; - static String m68(days, email) => + static String m69(days, email) => "You can access the account after ${days} days. A notification will be sent to ${email}."; - static String m69(email) => + static String m70(email) => "You can now recover ${email}\'s account by setting a new password."; - static String m70(email) => "${email} is trying to recover your account."; + static String m71(email) => "${email} is trying to recover your account."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Both of you get ${storageInGB} GB* free"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} will be removed from this shared album\n\nAny photos added by them will also be removed from the album"; - static String m73(endDate) => "Subscription renews on ${endDate}"; + static String m74(endDate) => "Subscription renews on ${endDate}"; - static String m74(name) => "Road trip with ${name}"; + static String m75(name) => "Road trip with ${name}"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} result found', other: '${count} results found')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Sections length mismatch: ${snapshotLength} != ${searchLength}"; - static String m77(count) => "${count} selected"; + static String m117(count) => "${count} selected"; - static String m78(count, yourCount) => + static String m78(count) => "${count} selected"; + + static String m79(count, yourCount) => "${count} selected (${yourCount} yours)"; - static String m79(name) => "Selfies with ${name}"; - - static String m80(verificationID) => - "Here\'s my verification ID: ${verificationID} for ente.io."; + static String m80(name) => "Selfies with ${name}"; static String m81(verificationID) => + "Here\'s my verification ID: ${verificationID} for ente.io."; + + static String m82(verificationID) => "Hey, can you confirm that this is your ente.io verification ID: ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Ente referral code: ${referralCode} \n\nApply it in Settings → General → Referrals to get ${referralStorageInGB} GB free after you signup for a paid plan\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Share with specific people', one: 'Shared with 1 person', other: 'Shared with ${numberOfPeople} people')}"; - static String m84(emailIDs) => "Shared with ${emailIDs}"; - - static String m85(fileType) => - "This ${fileType} will be deleted from your device."; + static String m85(emailIDs) => "Shared with ${emailIDs}"; static String m86(fileType) => + "This ${fileType} will be deleted from your device."; + + static String m87(fileType) => "This ${fileType} is in both Ente and your device."; - static String m87(fileType) => "This ${fileType} will be deleted from Ente."; + static String m88(fileType) => "This ${fileType} will be deleted from Ente."; - static String m88(name) => "Sports with ${name}"; + static String m89(name) => "Sports with ${name}"; - static String m89(name) => "Spotlight on ${name}"; + static String m90(name) => "Spotlight on ${name}"; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} of ${totalAmount} ${totalStorageUnit} used"; - static String m92(id) => + static String m93(id) => "Your ${id} is already linked to another Ente account.\nIf you would like to use your ${id} with this account, please contact our support\'\'"; - static String m93(endDate) => + static String m94(endDate) => "Your subscription will be cancelled on ${endDate}"; - static String m94(completed, total) => + static String m95(completed, total) => "${completed}/${total} memories preserved"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "Tap to upload, upload is currently ignored due to ${ignoreReason}"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "They also get ${storageAmountInGB} GB"; - static String m97(email) => "This is ${email}\'s Verification ID"; + static String m98(email) => "This is ${email}\'s Verification ID"; - static String m98(count) => + static String m99(count) => "${Intl.plural(count, one: 'This week, ${count} year ago', other: 'This week, ${count} years ago')}"; - static String m99(dateFormat) => "${dateFormat} through the years"; + static String m100(dateFormat) => "${dateFormat} through the years"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Soon', one: '1 day', other: '${count} days')}"; - static String m101(year) => "Trip in ${year}"; + static String m102(year) => "Trip in ${year}"; - static String m102(location) => "Trip to ${location}"; + static String m103(location) => "Trip to ${location}"; - static String m103(email) => + static String m104(email) => "You have been invited to be a legacy contact by ${email}."; - static String m104(galleryType) => + static String m105(galleryType) => "Type of gallery ${galleryType} is not supported for rename"; - static String m105(ignoreReason) => + static String m106(ignoreReason) => "Upload is ignored due to ${ignoreReason}"; - static String m106(count) => "Preserving ${count} memories..."; + static String m107(count) => "Preserving ${count} memories..."; - static String m107(endDate) => "Valid till ${endDate}"; + static String m108(endDate) => "Valid till ${endDate}"; - static String m108(email) => "Verify ${email}"; + static String m109(email) => "Verify ${email}"; - static String m109(count) => - "${Intl.plural(count, zero: 'Added 0 viewers', one: 'Added 1 viewer', other: 'Added ${count} viewers')}"; - - static String m110(email) => "We have sent a mail to ${email}"; + static String m110(name) => "View ${name} to unlink"; static String m111(count) => + "${Intl.plural(count, zero: 'Added 0 viewers', one: 'Added 1 viewer', other: 'Added ${count} viewers')}"; + + static String m112(email) => "We have sent a mail to ${email}"; + + static String m113(count) => "${Intl.plural(count, one: '${count} year ago', other: '${count} years ago')}"; - static String m112(name) => "You and ${name}"; + static String m114(name) => "You and ${name}"; - static String m113(storageSaved) => + static String m115(storageSaved) => "You have successfully freed up ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -332,11 +341,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Welcome back!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( "I understand that if I lose my password, I may lose my data since my data is end-to-end encrypted."), + "actionNotSupportedOnFavouritesAlbum": + MessageLookupByLibrary.simpleMessage( + "Action not supported on Favourites album"), "activeSessions": MessageLookupByLibrary.simpleMessage("Active sessions"), "add": MessageLookupByLibrary.simpleMessage("Add"), "addAName": MessageLookupByLibrary.simpleMessage("Add a name"), "addANewEmail": MessageLookupByLibrary.simpleMessage("Add a new email"), + "addAlbumWidgetPrompt": MessageLookupByLibrary.simpleMessage( + "Add an album widget to your homescreen and come back here to customize."), "addCollaborator": MessageLookupByLibrary.simpleMessage("Add collaborator"), "addCollaborators": m1, @@ -346,6 +360,8 @@ class MessageLookup extends MessageLookupByLibrary { "addItem": m2, "addLocation": MessageLookupByLibrary.simpleMessage("Add location"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Add"), + "addMemoriesWidgetPrompt": MessageLookupByLibrary.simpleMessage( + "Add a memories widget to your homescreen and come back here to customize."), "addMore": MessageLookupByLibrary.simpleMessage("Add more"), "addName": MessageLookupByLibrary.simpleMessage("Add name"), "addNameOrMerge": @@ -356,6 +372,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Details of add-ons"), "addOnValidTill": m3, "addOns": MessageLookupByLibrary.simpleMessage("Add-ons"), + "addParticipants": + MessageLookupByLibrary.simpleMessage("Add participants"), + "addPeopleWidgetPrompt": MessageLookupByLibrary.simpleMessage( + "Add a people widget to your homescreen and come back here to customize."), "addPhotos": MessageLookupByLibrary.simpleMessage("Add photos"), "addSelected": MessageLookupByLibrary.simpleMessage("Add selected"), "addToAlbum": MessageLookupByLibrary.simpleMessage("Add to album"), @@ -386,6 +406,8 @@ class MessageLookup extends MessageLookupByLibrary { "albumTitle": MessageLookupByLibrary.simpleMessage("Album title"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Album updated"), "albums": MessageLookupByLibrary.simpleMessage("Albums"), + "albumsWidgetDesc": MessageLookupByLibrary.simpleMessage( + "Select the albums you wish to see on your homescreen."), "allClear": MessageLookupByLibrary.simpleMessage("✨ All clear"), "allMemoriesPreserved": MessageLookupByLibrary.simpleMessage("All memories preserved"), @@ -533,22 +555,6 @@ class MessageLookup extends MessageLookupByLibrary { "blackFridaySale": MessageLookupByLibrary.simpleMessage("Black Friday Sale"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLBulkEdit": MessageLookupByLibrary.simpleMessage("Bulk Edit dates"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "You can now select multiple photos, and edit date/time for all of them with one quick action. Shifting dates is also supported."), - "cLFamilyPlan": - MessageLookupByLibrary.simpleMessage("Family Plan Limits"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "You can now set limits on how much storage your family members can use."), - "cLIcon": MessageLookupByLibrary.simpleMessage("New Icon"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Finally, a new app icon, that we think best represents our work. We\'ve also added an icon-switcher so you can continue using the old icon."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Memories"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Rediscover your special moments - spotlight on your favorite people, your trips and holidays, your best clicks, and much more. Turn on machine learning, tag yourself and name your friends for the best experience."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widgets"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Home screen widgets that are integrated with memories are now available. They will show your special moments without opening the app."), "cachedData": MessageLookupByLibrary.simpleMessage("Cached data"), "calculating": MessageLookupByLibrary.simpleMessage("Calculating..."), "canNotOpenBody": MessageLookupByLibrary.simpleMessage( @@ -617,6 +623,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("• Click"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage( "• Click on the overflow menu"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Close"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage("Club by capture time"), @@ -762,6 +770,7 @@ class MessageLookup extends MessageLookupByLibrary { "deleteItemCount": m21, "deleteLocation": MessageLookupByLibrary.simpleMessage("Delete location"), + "deleteMultipleAlbumDialog": m116, "deletePhotos": MessageLookupByLibrary.simpleMessage("Delete photos"), "deleteProgress": m22, "deleteReason1": MessageLookupByLibrary.simpleMessage( @@ -848,6 +857,7 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m25, "duplicateItemsGroup": m26, "edit": MessageLookupByLibrary.simpleMessage("Edit"), + "editEmailAlreadyLinked": m27, "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("Edit location"), @@ -861,16 +871,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("Email"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("Email already registered."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("Email not registered."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Email verification"), "emailYourLogs": MessageLookupByLibrary.simpleMessage("Email your logs"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Emergency Contacts"), "empty": MessageLookupByLibrary.simpleMessage("Empty"), @@ -930,6 +940,8 @@ class MessageLookup extends MessageLookupByLibrary { "Please enter a valid email address."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage("Enter your email address"), + "enterYourNewEmailAddress": MessageLookupByLibrary.simpleMessage( + "Enter your new email address"), "enterYourPassword": MessageLookupByLibrary.simpleMessage("Enter your password"), "enterYourRecoveryKey": @@ -945,7 +957,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Export your data"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Extra photos found"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Face not clustered yet, please come back later"), "faceRecognition": @@ -982,7 +994,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("FAQ"), "faqs": MessageLookupByLibrary.simpleMessage("FAQs"), "favorite": MessageLookupByLibrary.simpleMessage("Favorite"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Feedback"), "file": MessageLookupByLibrary.simpleMessage("File"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -996,8 +1008,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("File types and names"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Files deleted"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Files saved to gallery"), @@ -1014,26 +1026,26 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Free storage claimed"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Free storage usable"), "freeTrial": MessageLookupByLibrary.simpleMessage("Free trial"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Free up device space"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Save space on your device by clearing files that have been already backed up."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Free up space"), - "freeUpSpaceSaving": m39, + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Gallery"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Up to 1000 memories shown in gallery"), "general": MessageLookupByLibrary.simpleMessage("General"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generating encryption keys..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Go to settings"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( @@ -1061,7 +1073,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Hide shared items from home gallery"), "hiding": MessageLookupByLibrary.simpleMessage("Hiding..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("Hosted at OSM France"), "howItWorks": MessageLookupByLibrary.simpleMessage("How it works"), @@ -1116,7 +1128,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "It looks like something went wrong. Please retry after some time. If the error persists, please contact our support team."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Items show the number of days remaining before permanent deletion"), @@ -1136,7 +1148,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Kindly help us with this information"), "language": MessageLookupByLibrary.simpleMessage("Language"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Last updated"), "lastYearsTrip": MessageLookupByLibrary.simpleMessage("Last year\'s trip"), @@ -1149,7 +1161,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Legacy"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Legacy accounts"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Legacy allows trusted contacts to access your account in your absence."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1165,7 +1177,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("for faster sharing"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Enabled"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expired"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Link expiry"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Link has expired"), @@ -1173,8 +1185,8 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Link person"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "for better sharing experience"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Live Photos"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "You can share your subscription with your family"), @@ -1234,6 +1246,8 @@ class MessageLookup extends MessageLookupByLibrary { "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Long-press on an item to view in full-screen"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("Loop video off"), "loopVideoOn": MessageLookupByLibrary.simpleMessage("Loop video on"), "lostDevice": MessageLookupByLibrary.simpleMessage("Lost device?"), @@ -1259,7 +1273,10 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Me"), - "memoryCount": m48, + "memories": MessageLookupByLibrary.simpleMessage("Memories"), + "memoriesWidgetDesc": MessageLookupByLibrary.simpleMessage( + "Select the kind of memories you wish to see on your homescreen."), + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Merchandise"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Merge with existing"), @@ -1290,13 +1307,13 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Most recent"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Most relevant"), "mountains": MessageLookupByLibrary.simpleMessage("Over the hills"), - "moveItem": m49, + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Move selected photos to one date"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Move to album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Move to hidden album"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Moved to trash"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage("Moving files to album..."), @@ -1310,6 +1327,7 @@ class MessageLookup extends MessageLookupByLibrary { "newAlbum": MessageLookupByLibrary.simpleMessage("New album"), "newLocation": MessageLookupByLibrary.simpleMessage("New location"), "newPerson": MessageLookupByLibrary.simpleMessage("New person"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newRange": MessageLookupByLibrary.simpleMessage("New range"), "newToEnte": MessageLookupByLibrary.simpleMessage("New to Ente"), "newest": MessageLookupByLibrary.simpleMessage("Newest"), @@ -1347,10 +1365,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("No results"), "noResultsFound": MessageLookupByLibrary.simpleMessage("No results found"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("No system lock found"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("Not this person?"), "nothingSharedWithYouYet": @@ -1363,7 +1381,12 @@ class MessageLookup extends MessageLookupByLibrary { "onEnte": MessageLookupByLibrary.simpleMessage( "On ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("On the road again"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayMemories": + MessageLookupByLibrary.simpleMessage("On this day memories"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Only them"), "oops": MessageLookupByLibrary.simpleMessage("Oops"), "oopsCouldNotSaveEdits": @@ -1392,7 +1415,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("Pairing complete"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage( "Verification is still pending"), "passkey": MessageLookupByLibrary.simpleMessage("Passkey"), @@ -1402,43 +1425,47 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Password changed successfully"), "passwordLock": MessageLookupByLibrary.simpleMessage("Password lock"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Password strength is calculated considering the length of the password, used characters, and whether or not the password appears in the top 10,000 most used passwords"), "passwordWarning": MessageLookupByLibrary.simpleMessage( "We don\'t store this password, so if you forget, we cannot decrypt your data"), + "pastYearsMemories": + MessageLookupByLibrary.simpleMessage("Past years\' memories"), "paymentDetails": MessageLookupByLibrary.simpleMessage("Payment details"), "paymentFailed": MessageLookupByLibrary.simpleMessage("Payment failed"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Unfortunately your payment failed. Please contact support and we\'ll help you out!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Pending items"), "pendingSync": MessageLookupByLibrary.simpleMessage("Pending sync"), "people": MessageLookupByLibrary.simpleMessage("People"), "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage("People using your code"), + "peopleWidgetDesc": MessageLookupByLibrary.simpleMessage( + "Select the people you wish to see on your homescreen."), "permDeleteWarning": MessageLookupByLibrary.simpleMessage( "All items in trash will be permanently deleted\n\nThis action cannot be undone"), "permanentlyDelete": MessageLookupByLibrary.simpleMessage("Permanently delete"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "Permanently delete from device?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Person name"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Furry companions"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Photo descriptions"), "photoGridSize": MessageLookupByLibrary.simpleMessage("Photo grid size"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("photo"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Photos"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Photos added by you will be removed from the album"), - "photosCount": m60, + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "Photos keep relative time difference"), @@ -1448,7 +1475,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("PIN lock"), "playOnTv": MessageLookupByLibrary.simpleMessage("Play album on TV"), "playOriginal": MessageLookupByLibrary.simpleMessage("Play original"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Play stream"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore subscription"), @@ -1461,14 +1488,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Please contact support if the problem persists"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Please grant permissions"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Please login again"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Please select quick links to remove"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Please try again"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1482,7 +1509,7 @@ class MessageLookup extends MessageLookupByLibrary { "Please wait for sometime before retrying"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Please wait, this will take a while."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("Preparing logs..."), "preserveMore": MessageLookupByLibrary.simpleMessage("Preserve more"), @@ -1501,7 +1528,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Proceed"), "processed": MessageLookupByLibrary.simpleMessage("Processed"), "processing": MessageLookupByLibrary.simpleMessage("Processing"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Processing videos"), "publicLinkCreated": @@ -1514,9 +1541,9 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Raise ticket"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Rate the app"), "rateUs": MessageLookupByLibrary.simpleMessage("Rate us"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("Reassign \"Me\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Reassigning..."), "recover": MessageLookupByLibrary.simpleMessage("Recover"), @@ -1527,7 +1554,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recover account"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Recovery initiated"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Recovery key"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Recovery key copied to clipboard"), @@ -1541,12 +1568,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recovery key verified"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Your recovery key is the only way to recover your photos if you forget your password. You can find your recovery key in Settings > Account.\n\nPlease enter your recovery key here to verify that you have saved it correctly."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Recovery successful!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "A trusted contact is trying to access your account"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "The current device is not powerful enough to verify your password, but we can regenerate in a way that works with all devices.\n\nPlease login using your recovery key and regenerate your password (you can use the same one again if you wish)."), "recreatePasswordTitle": @@ -1561,7 +1588,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Give this code to your friends"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. They sign up for a paid plan"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Referrals"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Referrals are currently paused"), @@ -1590,7 +1617,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Remove link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Remove participant"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Remove person label"), "removePublicLink": @@ -1610,7 +1637,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Rename file"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renew subscription"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Report a bug"), "reportBug": MessageLookupByLibrary.simpleMessage("Report bug"), "resendEmail": MessageLookupByLibrary.simpleMessage("Resend email"), @@ -1635,7 +1662,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Review suggestions"), "right": MessageLookupByLibrary.simpleMessage("Right"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Rotate"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Rotate left"), "rotateRight": MessageLookupByLibrary.simpleMessage("Rotate right"), @@ -1689,8 +1716,8 @@ class MessageLookup extends MessageLookupByLibrary { "Invite people, and you\'ll see all photos shared by them here"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "People will be shown here once processing and syncing is complete"), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Security"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "See public album links in app"), @@ -1728,6 +1755,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Select your face"), "selectYourPlan": MessageLookupByLibrary.simpleMessage("Select your plan"), + "selectedAlbums": m117, "selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage( "Selected files are not on Ente"), "selectedFoldersWillBeEncryptedAndBackedUp": @@ -1739,9 +1767,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Selected items will be removed from this person, but not deleted from your library."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, - "selfiesWithThem": m79, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Send"), "sendEmail": MessageLookupByLibrary.simpleMessage("Send email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Send invite"), @@ -1770,16 +1798,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Share an album now"), "shareLink": MessageLookupByLibrary.simpleMessage("Share link"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Share only with the people you want"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Download Ente so we can easily share original quality photos and videos\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage("Share with non-Ente users"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Share your first album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1790,7 +1818,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("New shared photos"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Receive notifications when someone adds a photo to a shared album that you\'re a part of"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Shared with me"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Shared with you"), @@ -1807,12 +1835,13 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sign out other devices"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "I agree to the terms of service and privacy policy"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "It will be deleted from all albums."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Skip"), + "smartMemories": MessageLookupByLibrary.simpleMessage("Smart memories"), "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": MessageLookupByLibrary.simpleMessage( @@ -1842,13 +1871,15 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Sorry, we could not generate secure keys on this device.\n\nplease sign up from a different device."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Sort"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Sort by"), "sortNewestFirst": MessageLookupByLibrary.simpleMessage("Newest first"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Oldest first"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Success"), - "sportsWithThem": m88, - "spotlightOnThem": m89, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Spotlight on yourself"), "startAccountRecoveryTitle": @@ -1862,14 +1893,14 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Storage"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Family"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("You"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Storage limit exceeded"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Stream details"), "strongStrength": MessageLookupByLibrary.simpleMessage("Strong"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Subscribe"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "You need an active paid subscription to enable sharing."), @@ -1887,7 +1918,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Suggest features"), "sunrise": MessageLookupByLibrary.simpleMessage("On the horizon"), "support": MessageLookupByLibrary.simpleMessage("Support"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Sync stopped"), "syncing": MessageLookupByLibrary.simpleMessage("Syncing..."), "systemTheme": MessageLookupByLibrary.simpleMessage("System"), @@ -1896,7 +1927,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Tap to enter code"), "tapToUnlock": MessageLookupByLibrary.simpleMessage("Tap to unlock"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Tap to upload"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "It looks like something went wrong. Please retry after some time. If the error persists, please contact our support team."), "terminate": MessageLookupByLibrary.simpleMessage("Terminate"), @@ -1919,7 +1950,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "These items will be deleted from your device."), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "They will be deleted from all albums."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1937,12 +1968,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("This image has no exif data"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("This is me!"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "This is your Verification ID"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("This week through the years"), - "thisWeekXYearsAgo": m98, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "This will log you out of the following device:"), @@ -1954,7 +1985,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "This will remove public links of all selected quick links."), - "throughTheYears": m99, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "To enable app lock, please setup device passcode or screen lock in your system settings."), @@ -1968,13 +1999,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Total size"), "trash": MessageLookupByLibrary.simpleMessage("Trash"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Trim"), - "tripInYear": m101, - "tripToLocation": m102, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Trusted contacts"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Try again"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Turn on backup to automatically upload files added to this device folder to Ente."), @@ -1992,7 +2023,7 @@ class MessageLookup extends MessageLookupByLibrary { "Two-factor authentication successfully reset"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Two-factor setup"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Unarchive"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Unarchive album"), @@ -2015,10 +2046,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Updating folder selection..."), "upgrade": MessageLookupByLibrary.simpleMessage("Upgrade"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage("Uploading files to album..."), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Preserving 1 memory..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -2036,7 +2067,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Use selected photo"), "usedSpace": MessageLookupByLibrary.simpleMessage("Used space"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verification failed, please try again"), @@ -2044,7 +2075,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Verification ID"), "verify": MessageLookupByLibrary.simpleMessage("Verify"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verify email"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verify"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verify passkey"), "verifyPassword": @@ -2055,7 +2086,7 @@ class MessageLookup extends MessageLookupByLibrary { "videoInfo": MessageLookupByLibrary.simpleMessage("Video Info"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("video"), "videoStreaming": - MessageLookupByLibrary.simpleMessage("Generate streamable video"), + MessageLookupByLibrary.simpleMessage("Streamable videos"), "videos": MessageLookupByLibrary.simpleMessage("Videos"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("View active sessions"), @@ -2067,10 +2098,11 @@ class MessageLookup extends MessageLookupByLibrary { "viewLargeFilesDesc": MessageLookupByLibrary.simpleMessage( "View files that are consuming the most amount of storage."), "viewLogs": MessageLookupByLibrary.simpleMessage("View logs"), + "viewPersonToUnlink": m110, "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("View recovery key"), "viewer": MessageLookupByLibrary.simpleMessage("Viewer"), - "viewersSuccessfullyAdded": m109, + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Please visit web.ente.io to manage your subscription"), "waitingForVerification": @@ -2083,15 +2115,16 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "We don\'t support editing photos and albums that you don\'t own yet"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Weak"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Welcome back!"), "whatsNew": MessageLookupByLibrary.simpleMessage("What\'s new"), "whyAddTrustContact": MessageLookupByLibrary.simpleMessage( "Trusted contact can help in recovering your data."), + "widgets": MessageLookupByLibrary.simpleMessage("Widgets"), "yearShort": MessageLookupByLibrary.simpleMessage("yr"), "yearly": MessageLookupByLibrary.simpleMessage("Yearly"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Yes"), "yesCancel": MessageLookupByLibrary.simpleMessage("Yes, cancel"), "yesConvertToViewer": @@ -2105,7 +2138,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage("Yes, reset person"), "you": MessageLookupByLibrary.simpleMessage("You"), - "youAndThem": m112, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("You are on a family plan!"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage( @@ -2124,7 +2157,7 @@ class MessageLookup extends MessageLookupByLibrary { "You cannot share with yourself"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "You don\'t have any archived items."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Your account has been deleted"), "yourMap": MessageLookupByLibrary.simpleMessage("Your map"), diff --git a/mobile/lib/generated/intl/messages_es.dart b/mobile/lib/generated/intl/messages_es.dart index a454c25b56..dacf05f0f5 100644 --- a/mobile/lib/generated/intl/messages_es.dart +++ b/mobile/lib/generated/intl/messages_es.dart @@ -100,225 +100,225 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} archivos, ${formattedSize} cada uno"; - static String m27(newEmail) => "Correo cambiado a ${newEmail}"; + static String m28(newEmail) => "Correo cambiado a ${newEmail}"; - static String m28(email) => "${email} no tiene una cuenta de Ente."; + static String m29(email) => "${email} no tiene una cuenta de Ente."; - static String m29(email) => + static String m30(email) => "${email} no tiene una cuente en Ente.\n\nEnvíale una invitación para compartir fotos."; - static String m30(name) => "Abrazando a ${name}"; + static String m31(name) => "Abrazando a ${name}"; - static String m31(text) => "Fotos adicionales encontradas para ${text}"; + static String m32(text) => "Fotos adicionales encontradas para ${text}"; - static String m32(name) => "Festejando con ${name}"; - - static String m33(count, formattedNumber) => - "Se ha realizado la copia de seguridad de ${Intl.plural(count, one: '1 archivo', other: '${formattedNumber} archivos')} de este dispositivo de forma segura"; + static String m33(name) => "Festejando con ${name}"; static String m34(count, formattedNumber) => + "Se ha realizado la copia de seguridad de ${Intl.plural(count, one: '1 archivo', other: '${formattedNumber} archivos')} de este dispositivo de forma segura"; + + static String m35(count, formattedNumber) => "Se ha realizado la copia de seguridad de ${Intl.plural(count, one: '1 archivo', other: '${formattedNumber} archivos')} de este álbum de forma segura"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB cada vez que alguien se registra en un plan de pago y aplica tu código"; - static String m36(endDate) => "Prueba gratuita válida hasta ${endDate}"; + static String m37(endDate) => "Prueba gratuita válida hasta ${endDate}"; - static String m37(count) => + static String m38(count) => "Aún puedes acceder ${Intl.plural(count, one: 'a él', other: 'a ellos')} en Ente mientras tengas una suscripción activa"; - static String m38(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; - static String m39(count, formattedSize) => + static String m40(count, formattedSize) => "${Intl.plural(count, one: 'Se puede eliminar del dispositivo para liberar ${formattedSize}', other: 'Se pueden eliminar del dispositivo para liberar ${formattedSize}')}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Procesando ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Senderismo con ${name}"; + static String m42(name) => "Senderismo con ${name}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} elemento', other: '${count} elementos')}"; - static String m43(name) => "Última vez con ${name}"; + static String m44(name) => "Última vez con ${name}"; - static String m44(email) => + static String m45(email) => "${email} te ha invitado a ser un contacto de confianza"; - static String m45(expiryTime) => "El enlace caducará en ${expiryTime}"; + static String m46(expiryTime) => "El enlace caducará en ${expiryTime}"; - static String m46(email) => "Enlazar persona a ${email}"; + static String m47(email) => "Enlazar persona a ${email}"; - static String m47(personName, email) => + static String m48(personName, email) => "Esto enlazará a ${personName} a ${email}"; - static String m48(count, formattedCount) => + static String m49(count, formattedCount) => "${Intl.plural(count, zero: 'no hay recuerdos', one: '${formattedCount} recuerdo', other: '${formattedCount} recuerdos')}"; - static String m49(count) => + static String m50(count) => "${Intl.plural(count, one: 'Mover objeto', other: 'Mover objetos')}"; - static String m50(albumName) => "Movido exitosamente a ${albumName}"; + static String m51(albumName) => "Movido exitosamente a ${albumName}"; - static String m51(personName) => "No hay sugerencias para ${personName}"; + static String m52(personName) => "No hay sugerencias para ${personName}"; - static String m52(name) => "¿No es ${name}?"; + static String m53(name) => "¿No es ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Por favor, contacta a ${familyAdminEmail} para cambiar tu código."; - static String m54(name) => "Fiesta con ${name}"; + static String m55(name) => "Fiesta con ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Seguridad de la contraseña: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Por favor, habla con el soporte de ${providerName} si se te cobró"; - static String m57(name, age) => "¡${name} tiene ${age} años!"; + static String m58(name, age) => "¡${name} tiene ${age} años!"; - static String m58(name, age) => "${name} cumpliendo ${age} pronto"; - - static String m59(count) => - "${Intl.plural(count, zero: 'No hay fotos', one: '1 foto', other: '${count} fotos')}"; + static String m59(name, age) => "${name} cumpliendo ${age} pronto"; static String m60(count) => + "${Intl.plural(count, zero: 'No hay fotos', one: '1 foto', other: '${count} fotos')}"; + + static String m61(count) => "${Intl.plural(count, zero: '0 fotos', one: '1 foto', other: '${count} fotos')}"; - static String m61(endDate) => + static String m62(endDate) => "Prueba gratuita válida hasta ${endDate}.\nPuedes elegir un plan de pago después."; - static String m62(toEmail) => + static String m63(toEmail) => "Por favor, envíanos un correo electrónico a ${toEmail}"; - static String m63(toEmail) => "Por favor, envía los registros a ${toEmail}"; + static String m64(toEmail) => "Por favor, envía los registros a ${toEmail}"; - static String m64(name) => "Posando con ${name}"; + static String m65(name) => "Posando con ${name}"; - static String m65(folderName) => "Procesando ${folderName}..."; + static String m66(folderName) => "Procesando ${folderName}..."; - static String m66(storeName) => "Puntúanos en ${storeName}"; + static String m67(storeName) => "Puntúanos en ${storeName}"; - static String m67(name) => "Te has reasignado a ${name}"; + static String m68(name) => "Te has reasignado a ${name}"; - static String m68(days, email) => + static String m69(days, email) => "Puedes acceder a la cuenta después de ${days} días. Se enviará una notificación a ${email}."; - static String m69(email) => + static String m70(email) => "Ahora puedes recuperar la cuenta de ${email} estableciendo una nueva contraseña."; - static String m70(email) => "${email} está intentando recuperar tu cuenta."; + static String m71(email) => "${email} está intentando recuperar tu cuenta."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Ambos obtienen ${storageInGB} GB* gratis"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} será eliminado de este álbum compartido\n\nCualquier foto añadida por ellos también será eliminada del álbum"; - static String m73(endDate) => "La suscripción se renueva el ${endDate}"; + static String m74(endDate) => "La suscripción se renueva el ${endDate}"; - static String m74(name) => "Viaje en carretera con ${name}"; + static String m75(name) => "Viaje en carretera con ${name}"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} resultado encontrado', other: '${count} resultados encontrados')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "La longitud de las secciones no coincide: ${snapshotLength} != ${searchLength}"; - static String m77(count) => "${count} seleccionados"; + static String m78(count) => "${count} seleccionados"; - static String m78(count, yourCount) => + static String m79(count, yourCount) => "${count} seleccionados (${yourCount} tuyos)"; - static String m79(name) => "Selfies con ${name}"; - - static String m80(verificationID) => - "Aquí está mi ID de verificación: ${verificationID} para ente.io."; + static String m80(name) => "Selfies con ${name}"; static String m81(verificationID) => + "Aquí está mi ID de verificación: ${verificationID} para ente.io."; + + static String m82(verificationID) => "Hola, ¿puedes confirmar que esta es tu ID de verificación ente.io: ${verificationID}?"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Código de referido de Ente: ${referralCode} \n\nAñádelo en Ajustes → General → Referidos para obtener ${referralStorageInGB} GB gratis tras comprar un plan de pago.\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Compartir con personas específicas', one: 'Compartido con 1 persona', other: 'Compartido con ${numberOfPeople} personas')}"; - static String m84(emailIDs) => "Compartido con ${emailIDs}"; - - static String m85(fileType) => - "Este ${fileType} se eliminará de tu dispositivo."; + static String m85(emailIDs) => "Compartido con ${emailIDs}"; static String m86(fileType) => + "Este ${fileType} se eliminará de tu dispositivo."; + + static String m87(fileType) => "Este ${fileType} está tanto en Ente como en tu dispositivo."; - static String m87(fileType) => "Este ${fileType} será eliminado de Ente."; + static String m88(fileType) => "Este ${fileType} será eliminado de Ente."; - static String m88(name) => "Deportes con ${name}"; + static String m89(name) => "Deportes con ${name}"; - static String m89(name) => "Enfocar a ${name}"; + static String m90(name) => "Enfocar a ${name}"; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} de ${totalAmount} ${totalStorageUnit} usados"; - static String m92(id) => + static String m93(id) => "Tu ${id} ya está vinculada a otra cuenta de Ente.\nSi deseas utilizar tu ${id} con esta cuenta, ponte en contacto con nuestro servicio de asistencia\'\'"; - static String m93(endDate) => "Tu suscripción se cancelará el ${endDate}"; + static String m94(endDate) => "Tu suscripción se cancelará el ${endDate}"; - static String m94(completed, total) => + static String m95(completed, total) => "${completed}/${total} recuerdos conservados"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "Toca para subir, la subida se está ignorando debido a ${ignoreReason}"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "También obtienen ${storageAmountInGB} GB"; - static String m97(email) => "Este es el ID de verificación de ${email}"; + static String m98(email) => "Este es el ID de verificación de ${email}"; - static String m98(count) => + static String m99(count) => "${Intl.plural(count, one: 'Esta semana, hace ${count} año', other: 'Esta semana, hace ${count} años')}"; - static String m99(dateFormat) => "${dateFormat} a través de los años"; + static String m100(dateFormat) => "${dateFormat} a través de los años"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Pronto', one: '1 día', other: '${count} días')}"; - static String m101(year) => "Viaje en ${year}"; + static String m102(year) => "Viaje en ${year}"; - static String m102(location) => "Viaje a ${location}"; + static String m103(location) => "Viaje a ${location}"; - static String m103(email) => + static String m104(email) => "Has sido invitado a ser un contacto legado por ${email}."; - static String m104(galleryType) => + static String m105(galleryType) => "El tipo de galería ${galleryType} no es compatible con el renombrado"; - static String m105(ignoreReason) => + static String m106(ignoreReason) => "La subida se ignoró debido a ${ignoreReason}"; - static String m106(count) => "Preservando ${count} memorias..."; + static String m107(count) => "Preservando ${count} memorias..."; - static String m107(endDate) => "Válido hasta ${endDate}"; + static String m108(endDate) => "Válido hasta ${endDate}"; - static String m108(email) => "Verificar ${email}"; - - static String m109(count) => - "${Intl.plural(count, zero: '0 espectadores añadidos', one: '1 espectador añadido', other: '${count} espectadores añadidos')}"; - - static String m110(email) => - "Hemos enviado un correo a ${email}"; + static String m109(email) => "Verificar ${email}"; static String m111(count) => + "${Intl.plural(count, zero: '0 espectadores añadidos', one: '1 espectador añadido', other: '${count} espectadores añadidos')}"; + + static String m112(email) => + "Hemos enviado un correo a ${email}"; + + static String m113(count) => "${Intl.plural(count, one: 'Hace ${count} año', other: 'Hace ${count} años')}"; - static String m112(name) => "Tú y ${name}"; + static String m114(name) => "Tú y ${name}"; - static String m113(storageSaved) => + static String m115(storageSaved) => "¡Has liberado ${storageSaved} con éxito!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -552,23 +552,6 @@ class MessageLookup extends MessageLookupByLibrary { "blackFridaySale": MessageLookupByLibrary.simpleMessage("Oferta del Black Friday"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLBulkEdit": - MessageLookupByLibrary.simpleMessage("Edición masiva de fechas"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Ahora puedes seleccionar múltiples fotos y editar la fecha/hora para todas ellas con una acción rápida. También es posible cambiar las fechas."), - "cLFamilyPlan": - MessageLookupByLibrary.simpleMessage("Límites de plan familiar"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Ahora puede establecer límites en cuanto al almacenamiento que los miembros de tu familia pueden utilizar."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Nuevo ícono"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Por fin, un nuevo icono de la aplicación, que creemos que representa mejor nuestro trabajo. También hemos añadido una opción para que puedas seguir utilizando el icono anterior."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Recuerdos"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Redescubre tus momentos especiales: enfócate en tu gente favorita, tus viajes y vacaciones, tus mejores clics, y mucho más. Activa el aprendizaje de automático, etiquétate a ti mismo y etiqueta a tus amigos para la mejor experiencia."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widgets"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Ya están disponibles los widgets de pantalla de inicio con tus recuerdos. Podrás ver tus momentos especiales sin abrir la aplicación."), "cachedData": MessageLookupByLibrary.simpleMessage("Datos almacenados en caché"), "calculating": MessageLookupByLibrary.simpleMessage("Calculando..."), @@ -892,16 +875,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("Correo electrónico"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "Correo electrónico ya registrado."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage( "Correo electrónico no registrado."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage( "Verificación por correo electrónico"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( "Envía tus registros por correo electrónico"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Contactos de emergencia"), "empty": MessageLookupByLibrary.simpleMessage("Vaciar"), @@ -983,7 +966,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exportar tus datos"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Fotos adicionales encontradas"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Cara no agrupada todavía, por favor vuelve más tarde"), "faceRecognition": @@ -1022,7 +1005,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("Preguntas Frecuentes"), "faqs": MessageLookupByLibrary.simpleMessage("Preguntas frecuentes"), "favorite": MessageLookupByLibrary.simpleMessage("Favorito"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Sugerencias"), "file": MessageLookupByLibrary.simpleMessage("Archivo"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -1036,8 +1019,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Tipos de archivos"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Tipos de archivo y nombres"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Archivos eliminados"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( @@ -1055,26 +1038,26 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Caras encontradas"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Almacenamiento gratuito obtenido"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Almacenamiento libre disponible"), "freeTrial": MessageLookupByLibrary.simpleMessage("Prueba gratuita"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Liberar espacio del dispositivo"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Ahorra espacio en tu dispositivo limpiando archivos que tienen copia de seguridad."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Liberar espacio"), - "freeUpSpaceSaving": m39, + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Galería"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Hasta 1000 memorias mostradas en la galería"), "general": MessageLookupByLibrary.simpleMessage("General"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generando claves de cifrado..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Ir a Ajustes"), "googlePlayId": MessageLookupByLibrary.simpleMessage("ID de Google Play"), @@ -1104,7 +1087,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Ocultar elementos compartidos de la galería de inicio"), "hiding": MessageLookupByLibrary.simpleMessage("Ocultando..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("Alojado en OSM France"), "howItWorks": MessageLookupByLibrary.simpleMessage("Cómo funciona"), @@ -1161,7 +1144,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Parece que algo salió mal. Por favor, vuelve a intentarlo después de algún tiempo. Si el error persiste, ponte en contacto con nuestro equipo de soporte."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Los artículos muestran el número de días restantes antes de ser borrados permanente"), @@ -1182,7 +1165,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Por favor ayúdanos con esta información"), "language": MessageLookupByLibrary.simpleMessage("Idioma"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Última actualización"), "lastYearsTrip": @@ -1197,7 +1180,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Legado"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Cuentas legadas"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Legado permite a los contactos de confianza acceder a su cuenta en su ausencia."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1215,7 +1198,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("para compartir más rápido"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Habilitado"), "linkExpired": MessageLookupByLibrary.simpleMessage("Vencido"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Enlace vence"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("El enlace ha caducado"), @@ -1223,8 +1206,8 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Vincular persona"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "para una mejor experiencia compartida"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Foto en vivo"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Puedes compartir tu suscripción con tu familia"), @@ -1316,7 +1299,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Yo"), - "memoryCount": m48, + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Mercancías"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Combinar con existente"), @@ -1348,13 +1331,13 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Más reciente"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Más relevante"), "mountains": MessageLookupByLibrary.simpleMessage("Sobre las colinas"), - "moveItem": m49, + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Mover las fotos seleccionadas a una fecha"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Mover al álbum"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Mover al álbum oculto"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Movido a la papelera"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1408,10 +1391,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Sin resultados"), "noResultsFound": MessageLookupByLibrary.simpleMessage( "No se han encontrado resultados"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Bloqueo de sistema no encontrado"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("¿No es esta persona?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( @@ -1425,7 +1408,7 @@ class MessageLookup extends MessageLookupByLibrary { "En ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("De nuevo en la carretera"), - "onlyFamilyAdminCanChangeCode": m53, + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Solo ellos"), "oops": MessageLookupByLibrary.simpleMessage("Ups"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1456,7 +1439,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("Emparejamiento completo"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage( "La verificación aún está pendiente"), "passkey": MessageLookupByLibrary.simpleMessage("Clave de acceso"), @@ -1467,7 +1450,7 @@ class MessageLookup extends MessageLookupByLibrary { "Contraseña cambiada correctamente"), "passwordLock": MessageLookupByLibrary.simpleMessage("Bloqueo con contraseña"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "La fortaleza de la contraseña se calcula teniendo en cuenta la longitud de la contraseña, los caracteres utilizados, y si la contraseña aparece o no en el top 10.000 de contraseñas más usadas"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1477,7 +1460,7 @@ class MessageLookup extends MessageLookupByLibrary { "paymentFailed": MessageLookupByLibrary.simpleMessage("Pago fallido"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Lamentablemente tu pago falló. Por favor, ¡contacta con el soporte técnico y te ayudaremos!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Elementos pendientes"), "pendingSync": @@ -1491,22 +1474,22 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Borrar permanentemente"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "¿Eliminar permanentemente del dispositivo?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Nombre de la persona"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Compañeros peludos"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Descripciones de fotos"), "photoGridSize": MessageLookupByLibrary.simpleMessage( "Tamaño de la cuadrícula de fotos"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("foto"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Fotos"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Las fotos añadidas por ti serán removidas del álbum"), - "photosCount": m60, + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "Las fotos mantienen una diferencia de tiempo relativa"), @@ -1518,7 +1501,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Reproducir álbum en TV"), "playOriginal": MessageLookupByLibrary.simpleMessage("Reproducir original"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Reproducir transmisión"), "playstoreSubscription": @@ -1532,14 +1515,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Por favor, contacta a soporte técnico si el problema persiste"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Por favor, concede permiso"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage( "Por favor, vuelve a iniciar sesión"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Por favor, selecciona enlaces rápidos para eliminar"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Por favor, inténtalo nuevamente"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1554,7 +1537,7 @@ class MessageLookup extends MessageLookupByLibrary { "Por favor, espera un momento antes de volver a intentarlo"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Espera. Esto tardará un poco."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("Preparando registros..."), "preserveMore": MessageLookupByLibrary.simpleMessage("Preservar más"), @@ -1573,7 +1556,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Continuar"), "processed": MessageLookupByLibrary.simpleMessage("Procesado"), "processing": MessageLookupByLibrary.simpleMessage("Procesando"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Procesando vídeos"), "publicLinkCreated": @@ -1587,9 +1570,9 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Evalúa la aplicación"), "rateUs": MessageLookupByLibrary.simpleMessage("Califícanos"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("Reasignar \"Yo\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Reasignando..."), "recover": MessageLookupByLibrary.simpleMessage("Recuperar"), @@ -1600,7 +1583,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recuperar cuenta"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Recuperación iniciada"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Clave de recuperación"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1615,12 +1598,12 @@ class MessageLookup extends MessageLookupByLibrary { "Clave de recuperación verificada"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Tu clave de recuperación es la única forma de recuperar tus fotos si olvidas tu contraseña. Puedes encontrar tu clave de recuperación en Ajustes > Cuenta.\n\nPor favor, introduce tu clave de recuperación aquí para verificar que la has guardado correctamente."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("¡Recuperación exitosa!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Un contacto de confianza está intentando acceder a tu cuenta"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "El dispositivo actual no es lo suficientemente potente para verificar su contraseña, pero podemos regenerarla de una manera que funcione con todos los dispositivos.\n\nPor favor inicie sesión usando su clave de recuperación y regenere su contraseña (puede volver a utilizar la misma si lo desea)."), "recreatePasswordTitle": @@ -1635,7 +1618,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Dale este código a tus amigos"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Se suscriben a un plan de pago"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Referidos"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Las referencias están actualmente en pausa"), @@ -1666,7 +1649,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Eliminar enlace"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Quitar participante"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage( "Eliminar etiqueta de persona"), "removePublicLink": @@ -1686,7 +1669,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Renombrar archivo"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renovar suscripción"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Reportar un error"), "reportBug": MessageLookupByLibrary.simpleMessage("Reportar error"), "resendEmail": @@ -1712,7 +1695,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Revisar sugerencias"), "right": MessageLookupByLibrary.simpleMessage("Derecha"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Girar"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Girar a la izquierda"), @@ -1770,8 +1753,8 @@ class MessageLookup extends MessageLookupByLibrary { "Invita a gente y verás todas las fotos compartidas aquí"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Las personas se mostrarán aquí cuando se complete el procesado y la sincronización"), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Seguridad"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Ver enlaces del álbum público en la aplicación"), @@ -1822,9 +1805,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Los elementos seleccionados se eliminarán de esta persona, pero no se eliminarán de tu biblioteca."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, - "selfiesWithThem": m79, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Enviar"), "sendEmail": MessageLookupByLibrary.simpleMessage("Enviar correo electrónico"), @@ -1858,16 +1841,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Compartir un álbum ahora"), "shareLink": MessageLookupByLibrary.simpleMessage("Compartir enlace"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Comparte sólo con la gente que quieres"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Descarga Ente para que podamos compartir fácilmente fotos y videos en calidad original.\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Compartir con usuarios fuera de Ente"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Comparte tu primer álbum"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1879,7 +1862,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nuevas fotos compartidas"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Recibir notificaciones cuando alguien agrega una foto a un álbum compartido contigo"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Compartido conmigo"), "sharedWithYou": @@ -1898,11 +1881,11 @@ class MessageLookup extends MessageLookupByLibrary { "Cerrar la sesión de otros dispositivos"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Estoy de acuerdo con los términos del servicio y la política de privacidad"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Se borrará de todos los álbumes."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Omitir"), "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": @@ -1938,8 +1921,8 @@ class MessageLookup extends MessageLookupByLibrary { "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Más antiguos primero"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Éxito"), - "sportsWithThem": m88, - "spotlightOnThem": m89, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Enfócate a ti mismo"), "startAccountRecoveryTitle": @@ -1954,15 +1937,15 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Almacenamiento"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Familia"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Usted"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Límite de datos excedido"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Detalles de la transmisión"), "strongStrength": MessageLookupByLibrary.simpleMessage("Segura"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Suscribirse"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Necesitas una suscripción activa de pago para habilitar el compartir."), @@ -1980,7 +1963,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sugerir una característica"), "sunrise": MessageLookupByLibrary.simpleMessage("Sobre el horizonte"), "support": MessageLookupByLibrary.simpleMessage("Soporte"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronización detenida"), "syncing": MessageLookupByLibrary.simpleMessage("Sincronizando..."), @@ -1991,7 +1974,7 @@ class MessageLookup extends MessageLookupByLibrary { "tapToUnlock": MessageLookupByLibrary.simpleMessage("Toca para desbloquear"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Toca para subir"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Parece que algo salió mal. Por favor, vuelve a intentarlo después de algún tiempo. Si el error persiste, ponte en contacto con nuestro equipo de soporte."), "terminate": MessageLookupByLibrary.simpleMessage("Terminar"), @@ -2015,7 +1998,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Estos elementos se eliminarán de tu dispositivo."), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Se borrarán de todos los álbumes."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -2033,12 +2016,12 @@ class MessageLookup extends MessageLookupByLibrary { "Esta imagen no tiene datos exif"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("¡Este soy yo!"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Esta es tu ID de verificación"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage( "Esta semana a través de los años"), - "thisWeekXYearsAgo": m98, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Esto cerrará la sesión del siguiente dispositivo:"), @@ -2050,7 +2033,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Esto eliminará los enlaces públicos de todos los enlaces rápidos seleccionados."), - "throughTheYears": m99, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "Para habilitar el bloqueo de la aplicación, por favor configura el código de acceso del dispositivo o el bloqueo de pantalla en los ajustes del sistema."), @@ -2064,13 +2047,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Tamaño total"), "trash": MessageLookupByLibrary.simpleMessage("Papelera"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Ajustar duración"), - "tripInYear": m101, - "tripToLocation": m102, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Contactos de confianza"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Inténtalo de nuevo"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Activar la copia de seguridad para subir automáticamente archivos añadidos a la carpeta de este dispositivo a Ente."), @@ -2088,7 +2071,7 @@ class MessageLookup extends MessageLookupByLibrary { "Autenticación de doble factor restablecida con éxito"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Configuración de dos pasos"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Desarchivar"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Desarchivar álbum"), @@ -2113,10 +2096,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Actualizando la selección de carpeta..."), "upgrade": MessageLookupByLibrary.simpleMessage("Mejorar"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Subiendo archivos al álbum..."), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Preservando 1 memoria..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -2135,7 +2118,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Usar foto seleccionada"), "usedSpace": MessageLookupByLibrary.simpleMessage("Espacio usado"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verificación fallida, por favor inténtalo de nuevo"), @@ -2144,7 +2127,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyEmail": MessageLookupByLibrary.simpleMessage( "Verificar correo electrónico"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verificar clave de acceso"), @@ -2156,8 +2139,6 @@ class MessageLookup extends MessageLookupByLibrary { "videoInfo": MessageLookupByLibrary.simpleMessage("Información de video"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("vídeo"), - "videoStreaming": - MessageLookupByLibrary.simpleMessage("Transmisión de vídeo"), "videos": MessageLookupByLibrary.simpleMessage("Vídeos"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Ver sesiones activas"), @@ -2174,7 +2155,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Ver código de recuperación"), "viewer": MessageLookupByLibrary.simpleMessage("Espectador"), - "viewersSuccessfullyAdded": m109, + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Por favor, visita web.ente.io para administrar tu suscripción"), "waitingForVerification": @@ -2187,7 +2168,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "No admitimos la edición de fotos y álbumes que aún no son tuyos"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Poco segura"), "welcomeBack": MessageLookupByLibrary.simpleMessage("¡Bienvenido de nuevo!"), @@ -2196,7 +2177,7 @@ class MessageLookup extends MessageLookupByLibrary { "Un contacto de confianza puede ayudar a recuperar sus datos."), "yearShort": MessageLookupByLibrary.simpleMessage("año"), "yearly": MessageLookupByLibrary.simpleMessage("Anualmente"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Sí"), "yesCancel": MessageLookupByLibrary.simpleMessage("Sí, cancelar"), "yesConvertToViewer": @@ -2210,7 +2191,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage("Si, eliminar persona"), "you": MessageLookupByLibrary.simpleMessage("Tu"), - "youAndThem": m112, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("¡Estás en un plan familiar!"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage( @@ -2229,7 +2210,7 @@ class MessageLookup extends MessageLookupByLibrary { "No puedes compartir contigo mismo"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "No tienes ningún elemento archivado."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Tu cuenta ha sido eliminada"), "yourMap": MessageLookupByLibrary.simpleMessage("Tu mapa"), diff --git a/mobile/lib/generated/intl/messages_eu.dart b/mobile/lib/generated/intl/messages_eu.dart index c053d3cecd..1f72eb4901 100644 --- a/mobile/lib/generated/intl/messages_eu.dart +++ b/mobile/lib/generated/intl/messages_eu.dart @@ -39,64 +39,64 @@ class MessageLookup extends MessageLookupByLibrary { static String m24(supportEmail) => "Mesedez, bidali e-maila ${supportEmail}-era zure erregistratutako e-mail helbidetik"; - static String m29(email) => + static String m30(email) => "${email}-(e)k ez du Ente konturik. \n\nBidali gonbidapena argazkiak partekatzeko."; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB norbaitek ordainpeko plan batean sartzen denean zure kodea aplikatzen badu"; - static String m45(expiryTime) => + static String m46(expiryTime) => "Esteka epe honetan iraungiko da: ${expiryTime}"; - static String m48(count, formattedCount) => + static String m49(count, formattedCount) => "${Intl.plural(count, zero: 'oroitzapenik ez', one: 'oroitzapen ${formattedCount}', other: '${formattedCount} oroitzapen')}"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Mesedez, jarri harremanetan ${familyAdminEmail}-(r)ekin zure kodea aldatzeko."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Pasahitzaren indarra: ${passwordStrengthValue}"; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Bai zuk bai haiek ${storageInGB} GB* dohainik izango duzue"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} partekatutako album honetatik ezabatuko da \n\nHaiek gehitutako argazki guztiak ere ezabatuak izango dira albumetik"; - static String m77(count) => "${count} hautatuta"; + static String m78(count) => "${count} hautatuta"; - static String m78(count, yourCount) => + static String m79(count, yourCount) => "${count} hautatuta (${yourCount} zureak)"; - static String m80(verificationID) => + static String m81(verificationID) => "Hau da nire Egiaztatze IDa: ${verificationID} ente.io-rako."; - static String m81(verificationID) => + static String m82(verificationID) => "Ei, baieztatu ahal duzu hau dela zure ente.io Egiaztatze IDa?: ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Sartu erreferentzia kodea: ${referralCode}\n\nAplikatu hemen: Ezarpenak → Orokorra→ Erreferentziak, ${referralStorageInGB} GB dohainik izateko ordainpeko plan batean \n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Partekatu pertsona zehatz batzuekin', one: 'Partekatu pertsona batekin', other: 'Partekatu ${numberOfPeople} pertsonarekin')}"; - static String m85(fileType) => "${fileType} hau zure gailutik ezabatuko da."; + static String m86(fileType) => "${fileType} hau zure gailutik ezabatuko da."; - static String m86(fileType) => + static String m87(fileType) => "${fileType} hau Ente-n eta zure gailuan dago."; - static String m87(fileType) => "${fileType} hau Ente-tik ezabatuko da."; + static String m88(fileType) => "${fileType} hau Ente-tik ezabatuko da."; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "Haiek ere lortuko dute ${storageAmountInGB} GB"; - static String m97(email) => "Hau da ${email}-(r)en Egiaztatze IDa"; + static String m98(email) => "Hau da ${email}-(r)en Egiaztatze IDa"; - static String m108(email) => "Egiaztatu ${email}"; + static String m109(email) => "Egiaztatu ${email}"; - static String m110(email) => + static String m112(email) => "Mezua bidali dugu ${email} helbidera"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -301,7 +301,7 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-maila"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "Helbide hau badago erregistratuta lehendik."), - "emailNoEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage( "Helbide hau ez dago erregistratuta."), "encryption": MessageLookupByLibrary.simpleMessage("Zifratzea"), @@ -346,7 +346,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ahaztu pasahitza"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Debaldeko biltegiratzea eskatuta"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Debaldeko biltegiratzea erabilgarri"), "generatingEncryptionKeys": @@ -385,7 +385,7 @@ class MessageLookup extends MessageLookupByLibrary { "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Gailu muga"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Indarrean"), "linkExpired": MessageLookupByLibrary.simpleMessage("Iraungita"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Estekaren epemuga"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Esteka iraungi da"), @@ -406,7 +406,7 @@ class MessageLookup extends MessageLookupByLibrary { "Berrikusi eta garbitu katxe lokalaren biltegiratzea."), "manageLink": MessageLookupByLibrary.simpleMessage("Kudeatu esteka"), "manageParticipants": MessageLookupByLibrary.simpleMessage("Kudeatu"), - "memoryCount": m48, + "memoryCount": m49, "mlConsent": MessageLookupByLibrary.simpleMessage( "Aktibatu ikasketa automatikoa"), "mlConsentConfirmation": MessageLookupByLibrary.simpleMessage( @@ -427,7 +427,7 @@ class MessageLookup extends MessageLookupByLibrary { "noRecoveryKeyNoDecryption": MessageLookupByLibrary.simpleMessage( "Gure puntutik-puntura zifratze protokoloa dela eta, zure data ezin da deszifratu zure pasahitza edo berreskuratze giltzarik gabe"), "ok": MessageLookupByLibrary.simpleMessage("Ondo"), - "onlyFamilyAdminCanChangeCode": m53, + "onlyFamilyAdminCanChangeCode": m54, "oops": MessageLookupByLibrary.simpleMessage("Ai!"), "oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage( "Oops, zerbait txarto joan da"), @@ -438,7 +438,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Pasahitza zuzenki aldatuta"), "passwordLock": MessageLookupByLibrary.simpleMessage("Pasahitza blokeoa"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordWarning": MessageLookupByLibrary.simpleMessage( "Ezin dugu zure pasahitza gorde, beraz, ahazten baduzu, ezin dugu zure data deszifratu"), "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage( @@ -482,7 +482,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Eman kode hau zure lagunei"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Haiek ordainpeko plan batean sinatu behar dute"), - "referralStep3": m71, + "referralStep3": m72, "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Erreferentziak momentuz geldituta daude"), "remove": MessageLookupByLibrary.simpleMessage("Kendu"), @@ -493,7 +493,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Ezabatu esteka"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Kendu parte hartzailea"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePublicLink": MessageLookupByLibrary.simpleMessage("Ezabatu esteka publikoa"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -516,8 +516,8 @@ class MessageLookup extends MessageLookupByLibrary { "Eskaneatu barra kode hau zure autentifikazio aplikazioaz"), "selectReason": MessageLookupByLibrary.simpleMessage("Aukeratu arrazoia"), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "sendEmail": MessageLookupByLibrary.simpleMessage("Bidali mezua"), "sendInvite": MessageLookupByLibrary.simpleMessage("Bidali gonbidapena"), @@ -529,24 +529,24 @@ class MessageLookup extends MessageLookupByLibrary { "setupComplete": MessageLookupByLibrary.simpleMessage("Prestaketa burututa"), "shareALink": MessageLookupByLibrary.simpleMessage("Partekatu esteka"), - "shareMyVerificationID": m80, - "shareTextConfirmOthersVerificationID": m81, + "shareMyVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Jaitsi Ente argazkiak eta bideoak jatorrizko kalitatean errez partekatu ahal izateko \n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Partekatu Ente erabiltzen ez dutenekin"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( "Sortu partekatutako eta parte hartzeko albumak beste Ente erabiltzaileekin, debaldeko planak dituztenak barne."), "sharing": MessageLookupByLibrary.simpleMessage("Partekatzen..."), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Zerbitzu baldintzak eta pribatutasun politikak onartzen ditut"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Album guztietatik ezabatuko da."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "someoneSharingAlbumsWithYouShouldSeeTheSameId": MessageLookupByLibrary.simpleMessage( "Zurekin albumak partekatzen dituen norbaitek ID berbera ikusi beharko luke bere gailuan."), @@ -564,7 +564,7 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Tamalez, ezin dugu giltza segururik sortu gailu honetan. \n\nMesedez, eman izena beste gailu batetik."), - "storageInGB": m90, + "storageInGB": m91, "strongStrength": MessageLookupByLibrary.simpleMessage("Gogorra"), "subscribe": MessageLookupByLibrary.simpleMessage("Harpidetu"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( @@ -577,12 +577,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Saioa bukatu?"), "termsOfServicesTitle": MessageLookupByLibrary.simpleMessage("Baldintzak"), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "thisCanBeUsedToRecoverYourAccountIfYou": MessageLookupByLibrary.simpleMessage( "Hau zure kontua berreskuratzeko erabili ahal duzu, zure bigarren faktorea ahaztuz gero"), "thisDevice": MessageLookupByLibrary.simpleMessage("Gailu hau"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("Hau da zure Egiaztatze IDa"), "thisWillLogYouOutOfTheFollowingDevice": @@ -616,7 +616,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Egiaztatu"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Egiaztatu e-maila"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyPassword": MessageLookupByLibrary.simpleMessage("Egiaztatu pasahitza"), "verifyingRecoveryKey": MessageLookupByLibrary.simpleMessage( @@ -625,7 +625,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Ikusi berreskuratze kodea"), "viewer": MessageLookupByLibrary.simpleMessage("Ikuslea"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Ahula"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Ongi etorri berriro!"), diff --git a/mobile/lib/generated/intl/messages_fa.dart b/mobile/lib/generated/intl/messages_fa.dart index 69bee7df90..68bb60fc51 100644 --- a/mobile/lib/generated/intl/messages_fa.dart +++ b/mobile/lib/generated/intl/messages_fa.dart @@ -28,18 +28,18 @@ class MessageLookup extends MessageLookupByLibrary { static String m24(supportEmail) => "لطفا یک ایمیل از آدرس ایمیلی که ثبت نام کردید به ${supportEmail} ارسال کنید"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "قدرت رمز عبور: ${passwordStrengthValue}"; - static String m66(storeName) => "به ما در ${storeName} امتیاز دهید"; + static String m67(storeName) => "به ما در ${storeName} امتیاز دهید"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} از ${totalAmount} ${totalStorageUnit} استفاده شده"; - static String m108(email) => "تایید ${email}"; + static String m109(email) => "تایید ${email}"; - static String m110(email) => + static String m112(email) => "ما یک ایمیل به ${email} ارسال کرده‌ایم"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -270,7 +270,7 @@ class MessageLookup extends MessageLookupByLibrary { "password": MessageLookupByLibrary.simpleMessage("رمز عبور"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "رمز عبور با موفقیت تغییر کرد"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordWarning": MessageLookupByLibrary.simpleMessage( "ما این رمز عبور را ذخیره نمی‌کنیم، بنابراین اگر فراموش کنید، نمی‌توانیم اطلاعات شما را رمزگشایی کنیم"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("عکس"), @@ -290,7 +290,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("پشتیبان گیری خصوصی"), "privateSharing": MessageLookupByLibrary.simpleMessage("اشتراک گذاری خصوصی"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "recover": MessageLookupByLibrary.simpleMessage("بازیابی"), "recoverAccount": MessageLookupByLibrary.simpleMessage("بازیابی حساب کاربری"), @@ -366,7 +366,7 @@ class MessageLookup extends MessageLookupByLibrary { "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("خانوادگی"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("شما"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("قوی"), "support": MessageLookupByLibrary.simpleMessage("پشتیبانی"), "systemTheme": MessageLookupByLibrary.simpleMessage("سیستم"), @@ -407,7 +407,7 @@ class MessageLookup extends MessageLookupByLibrary { "از کلید بازیابی استفاده کنید"), "verify": MessageLookupByLibrary.simpleMessage("تایید"), "verifyEmail": MessageLookupByLibrary.simpleMessage("تایید ایمیل"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("تایید"), "verifyPassword": MessageLookupByLibrary.simpleMessage("تایید رمز عبور"), @@ -420,7 +420,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewer": MessageLookupByLibrary.simpleMessage("بیننده"), "weAreOpenSource": MessageLookupByLibrary.simpleMessage("ما متن‌باز هستیم!"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("ضعیف"), "welcomeBack": MessageLookupByLibrary.simpleMessage("خوش آمدید!"), "whatsNew": MessageLookupByLibrary.simpleMessage("تغییرات جدید"), diff --git a/mobile/lib/generated/intl/messages_fr.dart b/mobile/lib/generated/intl/messages_fr.dart index f33ec00c7e..a360ecc3a3 100644 --- a/mobile/lib/generated/intl/messages_fr.dart +++ b/mobile/lib/generated/intl/messages_fr.dart @@ -22,9 +22,15 @@ class MessageLookup extends MessageLookupByLibrary { static String m0(title) => "${title} (Moi)"; + static String m1(count) => + "${Intl.plural(count, zero: 'Ajouter un collaborateur', one: 'Ajouter un collaborateur', other: 'Ajouter des collaborateurs')}"; + static String m3(storageAmount, endDate) => "Votre extension de ${storageAmount} est valable jusqu\'au ${endDate}"; + static String m4(count) => + "${Intl.plural(count, zero: 'Ajouter un spectateur', one: 'Ajouter une spectateur', other: 'Ajouter des spectateurs')}"; + static String m5(emailOrName) => "Ajouté par ${emailOrName}"; static String m6(albumName) => "Ajouté avec succès à ${albumName}"; @@ -76,6 +82,9 @@ class MessageLookup extends MessageLookupByLibrary { static String m21(count) => "${Intl.plural(count, one: 'Supprimer le fichier', other: 'Supprimer ${count} fichiers')}"; + static String m116(count) => + "Supprimer également les photos (et les vidéos) présentes dans ces ${count} albums de tous les autres albums dont ils font partie ?"; + static String m22(currentlyDeleting, totalCount) => "Suppression de ${currentlyDeleting} / ${totalCount}"; @@ -91,206 +100,227 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} fichiers, ${formattedSize} chacun"; - static String m27(newEmail) => "L\'email a été changé par ${newEmail}"; + static String m27(name) => "Cet e-mail est déjà lié à ${name}."; - static String m28(email) => "${email} n\'a pas de compte Ente."; + static String m28(newEmail) => "L\'email a été changé par ${newEmail}"; - static String m29(email) => + static String m29(email) => "${email} n\'a pas de compte Ente."; + + static String m30(email) => "${email} n\'a pas de compte Ente.\n\nEnvoyez une invitation pour partager des photos."; - static String m30(name) => "Embrasse ${name}"; + static String m31(name) => "Embrasse ${name}"; - static String m31(text) => "Photos supplémentaires trouvées pour ${text}"; + static String m32(text) => "Photos supplémentaires trouvées pour ${text}"; - static String m32(name) => "Fête avec ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 fichier sur cet appareil a été sauvegardé en toute sécurité', other: '${formattedNumber} fichiers sur cet appareil ont été sauvegardés en toute sécurité')}"; + static String m33(name) => "Fête avec ${name}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 fichier sur cet appareil a été sauvegardé en toute sécurité', other: '${formattedNumber} fichiers sur cet appareil ont été sauvegardés en toute sécurité')}"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 fichier dans cet album a été sauvegardé en toute sécurité', other: '${formattedNumber} fichiers dans cet album ont été sauvegardés en toute sécurité')}"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} Go chaque fois que quelqu\'un s\'inscrit à une offre payante et applique votre code"; - static String m36(endDate) => "Essai gratuit valide jusqu’au ${endDate}"; + static String m37(endDate) => "Essai gratuit valide jusqu’au ${endDate}"; - static String m38(sizeInMBorGB) => "Libérer ${sizeInMBorGB}"; + static String m38(count) => + "Vous pouvez toujours ${Intl.plural(count, one: 'l\'', other: 'les')} accéder sur Ente tant que vous avez un abonnement actif"; - static String m40(currentlyProcessing, totalCount) => + static String m39(sizeInMBorGB) => "Libérer ${sizeInMBorGB}"; + + static String m40(count, formattedSize) => + "${Intl.plural(count, one: 'Il peut être supprimé de l\'appareil pour libérer ${formattedSize}', other: 'Ils peuvent être supprimés de l\'appareil pour libérer ${formattedSize}')}"; + + static String m41(currentlyProcessing, totalCount) => "Traitement en cours ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Randonnée avec ${name}"; + static String m42(name) => "Randonnée avec ${name}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} objet', other: '${count} objets')}"; - static String m43(name) => "Dernière fois avec ${name}"; + static String m44(name) => "Dernière fois avec ${name}"; - static String m44(email) => + static String m45(email) => "${email} vous a invité à être un contact de confiance"; - static String m45(expiryTime) => "Le lien expirera le ${expiryTime}"; + static String m46(expiryTime) => "Le lien expirera le ${expiryTime}"; - static String m46(email) => "Associer la personne à ${email}"; + static String m47(email) => "Associer la personne à ${email}"; - static String m47(personName, email) => + static String m48(personName, email) => "Cela va associer ${personName} à ${email}"; - static String m50(albumName) => "Déplacé avec succès vers ${albumName}"; + static String m49(count, formattedCount) => + "${Intl.plural(count, zero: 'aucun souvenir', one: '${formattedCount} souvenir', other: '${formattedCount} souvenirs')}"; - static String m51(personName) => "Aucune suggestion pour ${personName}"; + static String m51(albumName) => "Déplacé avec succès vers ${albumName}"; - static String m52(name) => "Pas ${name}?"; + static String m52(personName) => "Aucune suggestion pour ${personName}"; - static String m53(familyAdminEmail) => + static String m53(name) => "Pas ${name}?"; + + static String m54(familyAdminEmail) => "Veuillez contacter ${familyAdminEmail} pour modifier votre code."; - static String m54(name) => "En soirée avec ${name}"; + static String m55(name) => "En soirée avec ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Sécurité du mot de passe : ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Veuillez contacter le support ${providerName} si vous avez été facturé"; - static String m57(name, age) => "${name} a ${age}!"; + static String m58(name, age) => "${name} a ${age}!"; - static String m58(name, age) => "${name} aura bientôt ${age}"; + static String m59(name, age) => "${name} aura bientôt ${age}"; - static String m59(count) => + static String m60(count) => "${Intl.plural(count, zero: 'No photos', one: '1 photo', other: '${count} photos')}"; - static String m61(endDate) => + static String m61(count) => + "${Intl.plural(count, zero: '0 photo', one: '1 photo', other: '${count} photos')}"; + + static String m62(endDate) => "Essai gratuit valable jusqu\'à ${endDate}.\nVous pouvez choisir un plan payant par la suite."; - static String m62(toEmail) => "Merci de nous envoyer un email à ${toEmail}"; + static String m63(toEmail) => "Merci de nous envoyer un email à ${toEmail}"; - static String m63(toEmail) => "Envoyez les logs à ${toEmail}"; + static String m64(toEmail) => "Envoyez les logs à ${toEmail}"; - static String m64(name) => "Pose avec ${name}"; + static String m65(name) => "Pose avec ${name}"; - static String m65(folderName) => "Traitement de ${folderName}..."; + static String m66(folderName) => "Traitement de ${folderName}..."; - static String m66(storeName) => "Laissez une note sur ${storeName}"; + static String m67(storeName) => "Laissez une note sur ${storeName}"; - static String m67(name) => "Vous a réassigné à ${name}"; + static String m68(name) => "Vous a réassigné à ${name}"; - static String m68(days, email) => + static String m69(days, email) => "Vous pourrez accéder au compte d\'ici ${days} jours. Une notification sera envoyée à ${email}."; - static String m69(email) => + static String m70(email) => "Vous pouvez maintenant récupérer le compte de ${email} en définissant un nouveau mot de passe."; - static String m70(email) => "${email} tente de récupérer votre compte."; + static String m71(email) => "${email} tente de récupérer votre compte."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Vous recevez tous les deux ${storageInGB} Go* gratuits"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} sera retiré de cet album partagé\n\nToutes les photos ajoutées par eux seront également retirées de l\'album"; - static String m73(endDate) => "Renouvellement le ${endDate}"; + static String m74(endDate) => "Renouvellement le ${endDate}"; - static String m74(name) => "En route avec ${name}"; + static String m75(name) => "En route avec ${name}"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} résultat trouvé', other: '${count} résultats trouvés')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Incompatibilité de longueur des sections : ${snapshotLength} != ${searchLength}"; - static String m77(count) => "${count} sélectionné(s)"; + static String m117(count) => "${count} sélectionné(s)"; - static String m78(count, yourCount) => + static String m78(count) => "${count} sélectionné(s)"; + + static String m79(count, yourCount) => "${count} sélectionné(s) (${yourCount} à vous)"; - static String m79(name) => "Selfies avec ${name}"; - - static String m80(verificationID) => - "Voici mon ID de vérification : ${verificationID} pour ente.io."; + static String m80(name) => "Selfies avec ${name}"; static String m81(verificationID) => + "Voici mon ID de vérification : ${verificationID} pour ente.io."; + + static String m82(verificationID) => "Hé, pouvez-vous confirmer qu\'il s\'agit de votre ID de vérification ente.io : ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Code de parrainage Ente : ${referralCode} \n\nValidez le dans Paramètres → Général → Références pour obtenir ${referralStorageInGB} Go gratuitement après votre inscription à un plan payant\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Partagez avec des personnes spécifiques', one: 'Partagé avec 1 personne', other: 'Partagé avec ${numberOfPeople} personnes')}"; - static String m84(emailIDs) => "Partagé avec ${emailIDs}"; - - static String m85(fileType) => - "Elle ${fileType} sera supprimée de votre appareil."; + static String m85(emailIDs) => "Partagé avec ${emailIDs}"; static String m86(fileType) => + "Elle ${fileType} sera supprimée de votre appareil."; + + static String m87(fileType) => "Cette ${fileType} est à la fois sur ente et sur votre appareil."; - static String m87(fileType) => "Cette ${fileType} sera supprimée de l\'Ente."; + static String m88(fileType) => "Cette ${fileType} sera supprimée de l\'Ente."; - static String m88(name) => "Sports avec ${name}"; + static String m89(name) => "Sports avec ${name}"; - static String m89(name) => "Spotlight sur ${name}"; + static String m90(name) => "Spotlight sur ${name}"; - static String m90(storageAmountInGB) => "${storageAmountInGB} Go"; + static String m91(storageAmountInGB) => "${storageAmountInGB} Go"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} sur ${totalAmount} ${totalStorageUnit} utilisés"; - static String m92(id) => + static String m93(id) => "Votre ${id} est déjà lié à un autre compte Ente.\nSi vous souhaitez utiliser votre ${id} avec ce compte, veuillez contacter notre support"; - static String m93(endDate) => "Votre abonnement sera annulé le ${endDate}"; + static String m94(endDate) => "Votre abonnement sera annulé le ${endDate}"; - static String m94(completed, total) => + static String m95(completed, total) => "${completed}/${total} souvenirs sauvegardés"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "Appuyer pour envoyer, l\'envoi est actuellement ignoré en raison de ${ignoreReason}"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "Ils obtiennent aussi ${storageAmountInGB} Go"; - static String m97(email) => "Ceci est l\'ID de vérification de ${email}"; + static String m98(email) => "Ceci est l\'ID de vérification de ${email}"; - static String m98(count) => + static String m99(count) => "${Intl.plural(count, one: 'Cette semaine, ${count} il y a l\'année', other: 'Cette semaine, ${count} il y a des années')}"; - static String m99(dateFormat) => "${dateFormat} au fil des années"; + static String m100(dateFormat) => "${dateFormat} au fil des années"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Bientôt', one: '1 jour', other: '${count} jours')}"; - static String m101(year) => "Voyage en ${year}"; + static String m102(year) => "Voyage en ${year}"; - static String m102(location) => "Voyage vers ${location}"; + static String m103(location) => "Voyage vers ${location}"; - static String m103(email) => + static String m104(email) => "Vous avez été invité(e) à être un(e) héritier(e) par ${email}."; - static String m104(galleryType) => + static String m105(galleryType) => "Les galeries de type \'${galleryType}\' ne peuvent être renommées"; - static String m105(ignoreReason) => + static String m106(ignoreReason) => "L\'envoi est ignoré en raison de ${ignoreReason}"; - static String m106(count) => "Sauvegarde de ${count} souvenirs..."; + static String m107(count) => "Sauvegarde de ${count} souvenirs..."; - static String m107(endDate) => "Valable jusqu\'au ${endDate}"; + static String m108(endDate) => "Valable jusqu\'au ${endDate}"; - static String m108(email) => "Vérifier ${email}"; + static String m109(email) => "Vérifier ${email}"; - static String m110(email) => - "Nous avons envoyé un email à ${email}"; + static String m110(name) => "Voir ${name} pour délier"; static String m111(count) => + "${Intl.plural(count, zero: '0 spectateur ajouté', one: 'Un spectateur ajouté', other: '${count} spectateurs ajoutés')}"; + + static String m112(email) => + "Nous avons envoyé un email à ${email}"; + + static String m113(count) => "${Intl.plural(count, one: 'il y a ${count} an', other: 'il y a ${count} ans')}"; - static String m112(name) => "Vous et ${name}"; + static String m114(name) => "Vous et ${name}"; - static String m113(storageSaved) => + static String m115(storageSaved) => "Vous avez libéré ${storageSaved} avec succès !"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -308,6 +338,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bon retour parmi nous !"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( "Je comprends que si je perds mon mot de passe, je perdrai mes données puisque mes données sont chiffrées de bout en bout."), + "actionNotSupportedOnFavouritesAlbum": + MessageLookupByLibrary.simpleMessage( + "Action non prise en charge sur l\'album des Favoris"), "activeSessions": MessageLookupByLibrary.simpleMessage("Sessions actives"), "add": MessageLookupByLibrary.simpleMessage("Ajouter"), @@ -316,6 +349,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ajouter un nouvel email"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Ajouter un collaborateur"), + "addCollaborators": m1, "addFiles": MessageLookupByLibrary.simpleMessage("Ajouter des fichiers"), "addFromDevice": @@ -335,6 +369,8 @@ class MessageLookup extends MessageLookupByLibrary { "addOnValidTill": m3, "addOns": MessageLookupByLibrary.simpleMessage("Modules complémentaires"), + "addParticipants": + MessageLookupByLibrary.simpleMessage("Ajouter des participants"), "addPhotos": MessageLookupByLibrary.simpleMessage("Ajouter des photos"), "addSelected": MessageLookupByLibrary.simpleMessage("Ajouter la sélection"), @@ -347,6 +383,7 @@ class MessageLookup extends MessageLookupByLibrary { "Ajouter un contact de confiance"), "addViewer": MessageLookupByLibrary.simpleMessage("Ajouter un observateur"), + "addViewers": m4, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage( "Ajoutez vos photos maintenant"), "addedAs": MessageLookupByLibrary.simpleMessage("Ajouté comme"), @@ -525,23 +562,6 @@ class MessageLookup extends MessageLookupByLibrary { "blackFridaySale": MessageLookupByLibrary.simpleMessage("Offre Black Friday"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLBulkEdit": MessageLookupByLibrary.simpleMessage( - "Dates de modification multiples"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Vous pouvez maintenant sélectionner plusieurs photos et modifier la date/heure pour toutes celles-ci, en une seule action rapide. Les dates de décalage sont également prises en charge."), - "cLFamilyPlan": MessageLookupByLibrary.simpleMessage( - "Limites pour le forfait Famille"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Vous pouvez maintenant fixer des limites sur la quantité de stockage que les membres de votre famille peuvent utiliser."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Nouvel icône"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Finalement, création d\'un nouvel icône d\'application qui, selon nous, représente au mieux notre travail. Nous avons également ajouté un changeur d\'icône pour que vous puissiez continuer à utiliser l\'ancien."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Souvenirs"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Redécouvrez vos précieux souvenirs - focus sur vos connaissances préférées, vos voyages et vos vacances, vos meilleurs clics et bien plus encore. Activez l\'apprentissage automatique, taguez-vous et nommez vos amis pour une meilleure expérience."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widgets"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Les widgets (ou gadgets) de l\'écran d\'accueil, qui sont intégrés à des souvenirs, sont maintenant disponibles. Ils montreront vos moments spéciaux sans nécessité d\'ouvrir l\'application."), "cachedData": MessageLookupByLibrary.simpleMessage("Données mises en cache"), "calculating": @@ -767,6 +787,7 @@ class MessageLookup extends MessageLookupByLibrary { "deleteItemCount": m21, "deleteLocation": MessageLookupByLibrary.simpleMessage("Supprimer la localisation"), + "deleteMultipleAlbumDialog": m116, "deletePhotos": MessageLookupByLibrary.simpleMessage("Supprimer des photos"), "deleteProgress": m22, @@ -859,6 +880,7 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m25, "duplicateItemsGroup": m26, "edit": MessageLookupByLibrary.simpleMessage("Éditer"), + "editEmailAlreadyLinked": m27, "editLocation": MessageLookupByLibrary.simpleMessage("Modifier l’emplacement"), "editLocationTagTitle": @@ -875,16 +897,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-mail"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("Email déjà enregistré."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("E-mail non enregistré."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage( "Authentification à deux facteurs par email"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( "Envoyez vos journaux par email"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Contacts d\'urgence"), "empty": MessageLookupByLibrary.simpleMessage("Vider"), @@ -946,6 +968,8 @@ class MessageLookup extends MessageLookupByLibrary { "Veuillez entrer une adresse email valide."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage("Entrez votre adresse e-mail"), + "enterYourNewEmailAddress": MessageLookupByLibrary.simpleMessage( + "Entrez votre nouvelle adresse e-mail"), "enterYourPassword": MessageLookupByLibrary.simpleMessage("Entrez votre mot de passe"), "enterYourRecoveryKey": MessageLookupByLibrary.simpleMessage( @@ -962,7 +986,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exportez vos données"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Photos supplémentaires trouvées"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Ce visage n\'a pas encore été regroupé, veuillez revenir plus tard"), "faceRecognition": @@ -1001,7 +1025,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("FAQ"), "faqs": MessageLookupByLibrary.simpleMessage("FAQ"), "favorite": MessageLookupByLibrary.simpleMessage("Favori"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Commentaires"), "file": MessageLookupByLibrary.simpleMessage("Fichier"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -1015,8 +1039,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Types de fichiers"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Types et noms de fichiers"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Fichiers supprimés"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( @@ -1034,25 +1058,27 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Visages trouvés"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Stockage gratuit obtenu"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Stockage gratuit disponible"), "freeTrial": MessageLookupByLibrary.simpleMessage("Essai gratuit"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Libérer de l\'espace sur l\'appareil"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Économisez de l\'espace sur votre appareil en effaçant les fichiers qui ont déjà été sauvegardés."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Libérer de l\'espace"), + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Galerie"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Jusqu\'à 1000 souvenirs affichés dans la galerie"), "general": MessageLookupByLibrary.simpleMessage("Général"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Génération des clés de chiffrement..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Allez aux réglages"), "googlePlayId": @@ -1083,7 +1109,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Masquer les éléments partagés avec vous dans la galerie"), "hiding": MessageLookupByLibrary.simpleMessage("Masquage en cours..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("Hébergé chez OSM France"), "howItWorks": @@ -1143,7 +1169,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Il semble qu\'une erreur s\'est produite. Veuillez réessayer après un certain temps. Si l\'erreur persiste, veuillez contacter notre équipe d\'assistance."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Les éléments montrent le nombre de jours restants avant la suppression définitive"), @@ -1165,7 +1191,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Merci de nous aider avec cette information"), "language": MessageLookupByLibrary.simpleMessage("Langue"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Dernière mise à jour"), "lastYearsTrip": @@ -1180,7 +1206,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Héritage"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Comptes hérités"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "L\'héritage permet aux contacts de confiance d\'accéder à votre compte en votre absence."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1197,7 +1223,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("pour un partage plus rapide"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Activé"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expiré"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Expiration du lien"), "linkHasExpired": @@ -1206,11 +1232,13 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Lier la personne"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "pour une meilleure expérience de partage"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Photos en direct"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Vous pouvez partager votre abonnement avec votre famille"), + "loadMessage2": MessageLookupByLibrary.simpleMessage( + "Nous avons préservé plus de 200 millions de souvenirs jusqu\'à présent"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Nous conservons 3 copies de vos données, l\'une dans un abri anti-atomique"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1296,6 +1324,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Moi"), + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Boutique"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Fusionner avec existant"), @@ -1335,7 +1364,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Déplacer vers l\'album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Déplacer vers un album masqué"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Déplacé dans la corbeille"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1389,10 +1418,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Aucun résultat"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Aucun résultat trouvé"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("Aucun verrou système trouvé"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage( "Ce n\'est pas cette personne ?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( @@ -1406,7 +1435,8 @@ class MessageLookup extends MessageLookupByLibrary { "Sur Ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("De nouveau sur la route"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("Ce jour-ci"), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Seulement eux"), "oops": MessageLookupByLibrary.simpleMessage("Oups"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1438,7 +1468,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("Appairage terminé"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage( "La vérification est toujours en attente"), "passkey": MessageLookupByLibrary.simpleMessage( @@ -1450,7 +1480,7 @@ class MessageLookup extends MessageLookupByLibrary { "Le mot de passe a été modifié"), "passwordLock": MessageLookupByLibrary.simpleMessage( "Verrouillage par mot de passe"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "La force du mot de passe est calculée en tenant compte de la longueur du mot de passe, des caractères utilisés et du fait que le mot de passe figure ou non parmi les 10 000 mots de passe les plus utilisés"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1461,7 +1491,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Échec du paiement"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Malheureusement votre paiement a échoué. Veuillez contacter le support et nous vous aiderons !"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Éléments en attente"), "pendingSync": @@ -1475,10 +1505,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Supprimer définitivement"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "Supprimer définitivement de l\'appareil ?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Nom de la personne"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Compagnons à quatre pattes"), "photoDescriptions": @@ -1486,11 +1516,12 @@ class MessageLookup extends MessageLookupByLibrary { "photoGridSize": MessageLookupByLibrary.simpleMessage("Taille de la grille photo"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("photo"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Photos"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Les photos ajoutées par vous seront retirées de l\'album"), + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "Les photos gardent une différence de temps relative"), @@ -1503,7 +1534,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Lire l\'album sur la TV"), "playOriginal": MessageLookupByLibrary.simpleMessage("Lire l\'original"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Lire le stream"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Abonnement au PlayStore"), @@ -1516,14 +1547,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Merci de contacter l\'assistance si cette erreur persiste"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Veuillez accorder la permission"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Veuillez vous reconnecter"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Veuillez sélectionner les liens rapides à supprimer"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Veuillez réessayer"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1538,7 +1569,7 @@ class MessageLookup extends MessageLookupByLibrary { "Veuillez attendre quelque temps avant de réessayer"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Veuillez patienter, cela prendra un peu de temps."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("Préparation des journaux..."), "preserveMore": MessageLookupByLibrary.simpleMessage("Conserver plus"), @@ -1558,7 +1589,7 @@ class MessageLookup extends MessageLookupByLibrary { "processed": MessageLookupByLibrary.simpleMessage("Appris"), "processing": MessageLookupByLibrary.simpleMessage("Traitement en cours"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Traitement des vidéos"), "publicLinkCreated": @@ -1572,10 +1603,10 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Évaluer l\'application"), "rateUs": MessageLookupByLibrary.simpleMessage("Évaluez-nous"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("Réassigner \"Moi\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Réassignation..."), "recover": MessageLookupByLibrary.simpleMessage("Récupérer"), @@ -1586,7 +1617,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Récupérer un compte"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Récupération initiée"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Clé de secours"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Clé de secours copiée dans le presse-papiers"), @@ -1600,12 +1631,12 @@ class MessageLookup extends MessageLookupByLibrary { "Clé de récupération vérifiée"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Votre clé de récupération est la seule façon de récupérer vos photos si vous oubliez votre mot de passe. Vous pouvez trouver votre clé de récupération dans Paramètres > Compte.\n\nVeuillez saisir votre clé de récupération ici pour vous assurer de l\'avoir enregistré correctement."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Restauration réussie !"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Un contact de confiance tente d\'accéder à votre compte"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "L\'appareil actuel n\'est pas assez puissant pour vérifier votre mot de passe, mais nous pouvons le régénérer d\'une manière qui fonctionne avec tous les appareils.\n\nVeuillez vous connecter à l\'aide de votre clé de secours et régénérer votre mot de passe (vous pouvez réutiliser le même si vous le souhaitez)."), "recreatePasswordTitle": @@ -1621,7 +1652,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Donnez ce code à vos ami·e·s"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Ils souscrivent à une offre payante"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Parrainages"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Les recommandations sont actuellement en pause"), @@ -1653,7 +1684,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Supprimer le lien"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Supprimer le participant"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage( "Supprimer le libellé d\'une personne"), "removePublicLink": @@ -1675,7 +1706,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Renommer le fichier"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renouveler l’abonnement"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Signaler un bogue"), "reportBug": MessageLookupByLibrary.simpleMessage("Signaler un bogue"), "resendEmail": @@ -1701,7 +1732,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Examiner les suggestions"), "right": MessageLookupByLibrary.simpleMessage("Droite"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Pivoter"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Pivoter à gauche"), "rotateRight": @@ -1761,8 +1792,8 @@ class MessageLookup extends MessageLookupByLibrary { "Invitez quelqu\'un·e et vous verrez ici toutes les photos partagées"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Les personnes seront affichées ici une fois le traitement terminé"), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Sécurité"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Ouvrir les liens des albums publics dans l\'application"), @@ -1804,6 +1835,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sélectionnez votre visage"), "selectYourPlan": MessageLookupByLibrary.simpleMessage("Sélectionner votre offre"), + "selectedAlbums": m117, "selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage( "Les fichiers sélectionnés ne sont pas sur Ente"), "selectedFoldersWillBeEncryptedAndBackedUp": @@ -1815,9 +1847,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Les éléments sélectionnés seront retirés de cette personne, mais pas supprimés de votre bibliothèque."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, - "selfiesWithThem": m79, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Envoyer"), "sendEmail": MessageLookupByLibrary.simpleMessage("Envoyer un e-mail"), "sendInvite": @@ -1851,16 +1883,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage( "Partagez un album maintenant"), "shareLink": MessageLookupByLibrary.simpleMessage("Partager le lien"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Partagez uniquement avec les personnes que vous souhaitez"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Téléchargez Ente pour pouvoir facilement partager des photos et vidéos en qualité originale\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Partager avec des utilisateurs non-Ente"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Partagez votre premier album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1871,7 +1903,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nouvelles photos partagées"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Recevoir des notifications quand quelqu\'un·e ajoute une photo à un album partagé dont vous faites partie"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Partagés avec moi"), "sharedWithYou": @@ -1891,11 +1923,11 @@ class MessageLookup extends MessageLookupByLibrary { "Déconnecter les autres appareils"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "J\'accepte les conditions d\'utilisation et la politique de confidentialité"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Elle sera supprimée de tous les albums."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Ignorer"), "social": MessageLookupByLibrary.simpleMessage("Retrouvez nous"), "someItemsAreInBothEnteAndYourDevice": @@ -1913,6 +1945,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Quelque chose s\'est mal passé, veuillez recommencer"), "sorry": MessageLookupByLibrary.simpleMessage("Désolé"), + "sorryBackupFailedDesc": MessageLookupByLibrary.simpleMessage( + "Désolé, nous n\'avons pas pu sauvegarder ce fichier maintenant, nous allons réessayer plus tard."), "sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage( "Désolé, impossible d\'ajouter aux favoris !"), "sorryCouldNotRemoveFromFavorites": @@ -1931,8 +1965,8 @@ class MessageLookup extends MessageLookupByLibrary { "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Plus ancien en premier"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Succès"), - "sportsWithThem": m88, - "spotlightOnThem": m89, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Éclairage sur vous-même"), "startAccountRecoveryTitle": @@ -1947,15 +1981,15 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Stockage"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Famille"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Vous"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Limite de stockage atteinte"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Détails du stream"), "strongStrength": MessageLookupByLibrary.simpleMessage("Forte"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("S\'abonner"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Vous avez besoin d\'un abonnement payant actif pour activer le partage."), @@ -1973,7 +2007,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Suggérer une fonctionnalité"), "sunrise": MessageLookupByLibrary.simpleMessage("À l\'horizon"), "support": MessageLookupByLibrary.simpleMessage("Support"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronisation arrêtée ?"), "syncing": MessageLookupByLibrary.simpleMessage( @@ -1986,7 +2020,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Appuyer pour déverrouiller"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Appuyer pour envoyer"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Il semble qu\'une erreur s\'est produite. Veuillez réessayer après un certain temps. Si l\'erreur persiste, veuillez contacter notre équipe d\'assistance."), "terminate": MessageLookupByLibrary.simpleMessage("Se déconnecter"), @@ -2010,7 +2044,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Ces éléments seront supprimés de votre appareil."), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Ils seront supprimés de tous les albums."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -2028,12 +2062,12 @@ class MessageLookup extends MessageLookupByLibrary { "Cette image n\'a pas de données exif"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("C\'est moi !"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Ceci est votre ID de vérification"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage( "Cette semaine au fil des années"), - "thisWeekXYearsAgo": m98, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Cela vous déconnectera de l\'appareil suivant :"), @@ -2045,7 +2079,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Ceci supprimera les liens publics de tous les liens rapides sélectionnés."), - "throughTheYears": m99, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "Pour activer le verrouillage de l\'application vous devez configurer le code d\'accès de l\'appareil ou le verrouillage de l\'écran dans les paramètres de votre système."), @@ -2059,13 +2093,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Taille totale"), "trash": MessageLookupByLibrary.simpleMessage("Corbeille"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Recadrer"), - "tripInYear": m101, - "tripToLocation": m102, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Contacts de confiance"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Réessayer"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Activez la sauvegarde pour charger automatiquement sur Ente les fichiers ajoutés à ce dossier de l\'appareil."), @@ -2085,7 +2119,7 @@ class MessageLookup extends MessageLookupByLibrary { "L\'authentification à deux facteurs a été réinitialisée avec succès "), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Configuration de l\'authentification à deux facteurs"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Désarchiver"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Désarchiver l\'album"), @@ -2113,10 +2147,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Mise à jour de la sélection du dossier..."), "upgrade": MessageLookupByLibrary.simpleMessage("Améliorer"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Envoi des fichiers vers l\'album..."), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage( "Sauvegarde d\'un souvenir..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -2134,7 +2168,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage( "Utiliser la photo sélectionnée"), "usedSpace": MessageLookupByLibrary.simpleMessage("Stockage utilisé"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "La vérification a échouée, veuillez réessayer"), @@ -2143,7 +2177,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Vérifier"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Vérifier l\'email"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Vérifier"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Vérifier la clé de sécurité"), @@ -2156,7 +2190,7 @@ class MessageLookup extends MessageLookupByLibrary { "videoInfo": MessageLookupByLibrary.simpleMessage("Informations vidéo"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("vidéo"), "videoStreaming": - MessageLookupByLibrary.simpleMessage("Streaming vidéo"), + MessageLookupByLibrary.simpleMessage("Vidéos diffusables"), "videos": MessageLookupByLibrary.simpleMessage("Vidéos"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage( "Afficher les connexions actives"), @@ -2171,9 +2205,11 @@ class MessageLookup extends MessageLookupByLibrary { "Affichez les fichiers qui consomment le plus de stockage."), "viewLogs": MessageLookupByLibrary.simpleMessage("Afficher les journaux"), + "viewPersonToUnlink": m110, "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Voir la clé de récupération"), "viewer": MessageLookupByLibrary.simpleMessage("Observateur"), + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Vous pouvez gérer votre abonnement sur web.ente.io"), "waitingForVerification": MessageLookupByLibrary.simpleMessage( @@ -2186,7 +2222,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Nous ne prenons pas en charge l\'édition des photos et des albums que vous ne possédez pas encore"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Securité Faible"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bienvenue !"), "whatsNew": MessageLookupByLibrary.simpleMessage("Nouveautés"), @@ -2194,7 +2230,7 @@ class MessageLookup extends MessageLookupByLibrary { "Un contact de confiance peut vous aider à récupérer vos données."), "yearShort": MessageLookupByLibrary.simpleMessage("an"), "yearly": MessageLookupByLibrary.simpleMessage("Annuel"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Oui"), "yesCancel": MessageLookupByLibrary.simpleMessage("Oui, annuler"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -2209,7 +2245,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage( "Oui, réinitialiser la personne"), "you": MessageLookupByLibrary.simpleMessage("Vous"), - "youAndThem": m112, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage( "Vous êtes sur un plan familial !"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage( @@ -2228,7 +2264,7 @@ class MessageLookup extends MessageLookupByLibrary { "Vous ne pouvez pas partager avec vous-même"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Vous n\'avez aucun élément archivé."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Votre compte a été supprimé"), "yourMap": MessageLookupByLibrary.simpleMessage("Votre carte"), diff --git a/mobile/lib/generated/intl/messages_he.dart b/mobile/lib/generated/intl/messages_he.dart index 014584079d..364f9a617e 100644 --- a/mobile/lib/generated/intl/messages_he.dart +++ b/mobile/lib/generated/intl/messages_he.dart @@ -57,67 +57,67 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} קבצים, כל אחד ${formattedSize}"; - static String m29(email) => + static String m30(email) => "לא נמצא חשבון ente ל-${email}.\n\nשלח להם הזמנה על מנת לשתף תמונות."; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB כל פעם שמישהו נרשם עבור תוכנית בתשלום ומחיל את הקוד שלך"; - static String m36(endDate) => "ניסיון חינם בתוקף עד ל-${endDate}"; + static String m37(endDate) => "ניסיון חינם בתוקף עד ל-${endDate}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} פריט', two: '${count} פריטים', many: '${count} פריטים', other: '${count} פריטים')}"; - static String m45(expiryTime) => "תוקף הקישור יפוג ב-${expiryTime}"; + static String m46(expiryTime) => "תוקף הקישור יפוג ב-${expiryTime}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "חוזק הסיסמא: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "אנא דבר עם התמיכה של ${providerName} אם אתה חוייבת"; - static String m66(storeName) => "דרג אותנו ב-${storeName}"; + static String m67(storeName) => "דרג אותנו ב-${storeName}"; - static String m71(storageInGB) => "3. שניכים מקבלים ${storageInGB} GB* בחינם"; + static String m72(storageInGB) => "3. שניכים מקבלים ${storageInGB} GB* בחינם"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} יוסר מהאלבום המשותף הזה\n\nגם תמונות שנוספו על ידיהם יוסרו מהאלבום"; - static String m77(count) => "${count} נבחרו"; + static String m78(count) => "${count} נבחרו"; - static String m78(count, yourCount) => "${count} נבחרו (${yourCount} שלך)"; - - static String m80(verificationID) => - "הנה מזהה האימות שלי: ${verificationID} עבור ente.io."; + static String m79(count, yourCount) => "${count} נבחרו (${yourCount} שלך)"; static String m81(verificationID) => + "הנה מזהה האימות שלי: ${verificationID} עבור ente.io."; + + static String m82(verificationID) => "היי, תוכל לוודא שזה מזהה האימות שלך של ente.io: ${verificationID}"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'שתף עם אנשים ספציפיים', one: 'שותף עם איש 1', two: 'שותף עם 2 אנשים', other: 'שותף עם ${numberOfPeople} אנשים')}"; - static String m84(emailIDs) => "הושתף ע\"י ${emailIDs}"; + static String m85(emailIDs) => "הושתף ע\"י ${emailIDs}"; - static String m85(fileType) => "${fileType} יימחק מהמכשיר שלך."; + static String m86(fileType) => "${fileType} יימחק מהמכשיר שלך."; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m93(endDate) => "המנוי שלך יבוטל ב-${endDate}"; + static String m94(endDate) => "המנוי שלך יבוטל ב-${endDate}"; - static String m94(completed, total) => "${completed}/${total} זכרונות נשמרו"; + static String m95(completed, total) => "${completed}/${total} זכרונות נשמרו"; - static String m96(storageAmountInGB) => "הם גם יקבלו ${storageAmountInGB} GB"; + static String m97(storageAmountInGB) => "הם גם יקבלו ${storageAmountInGB} GB"; - static String m97(email) => "זה מזהה האימות של ${email}"; + static String m98(email) => "זה מזהה האימות של ${email}"; - static String m108(email) => "אמת ${email}"; + static String m109(email) => "אמת ${email}"; - static String m110(email) => "שלחנו דוא\"ל ל${email}"; + static String m112(email) => "שלחנו דוא\"ל ל${email}"; - static String m111(count) => + static String m113(count) => "${Intl.plural(count, one: 'לפני ${count} שנה', two: 'לפני ${count} שנים', many: 'לפני ${count} שנים', other: 'לפני ${count} שנים')}"; - static String m113(storageSaved) => "הצלחת לפנות ${storageSaved}!"; + static String m115(storageSaved) => "הצלחת לפנות ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -396,7 +396,7 @@ class MessageLookup extends MessageLookupByLibrary { "edit": MessageLookupByLibrary.simpleMessage("ערוך"), "eligible": MessageLookupByLibrary.simpleMessage("זכאי"), "email": MessageLookupByLibrary.simpleMessage("דוא\"ל"), - "emailNoEnteAccount": m29, + "emailNoEnteAccount": m30, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("אימות מייל"), "empty": MessageLookupByLibrary.simpleMessage("ריק"), @@ -465,11 +465,11 @@ class MessageLookup extends MessageLookupByLibrary { "forgotPassword": MessageLookupByLibrary.simpleMessage("שכחתי סיסמה"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("מקום אחסון בחינם נתבע"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("מקום אחסון שמיש"), "freeTrial": MessageLookupByLibrary.simpleMessage("ניסיון חינמי"), - "freeTrialValidTill": m36, + "freeTrialValidTill": m37, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("פנה אחסון במכשיר"), "freeUpSpace": MessageLookupByLibrary.simpleMessage("פנה מקום"), @@ -507,7 +507,7 @@ class MessageLookup extends MessageLookupByLibrary { "invite": MessageLookupByLibrary.simpleMessage("הזמן"), "inviteYourFriends": MessageLookupByLibrary.simpleMessage("הזמן את חברייך"), - "itemCount": m42, + "itemCount": m43, "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "הפריטים שנבחרו יוסרו מהאלבום הזה"), "keepPhotos": MessageLookupByLibrary.simpleMessage("השאר תמונות"), @@ -529,7 +529,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("מגבלת כמות מכשירים"), "linkEnabled": MessageLookupByLibrary.simpleMessage("מאופשר"), "linkExpired": MessageLookupByLibrary.simpleMessage("פג תוקף"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("תאריך תפוגה ללינק"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("הקישור פג תוקף"), @@ -598,12 +598,12 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("הססמה הוחלפה בהצלחה"), "passwordLock": MessageLookupByLibrary.simpleMessage("נעילת סיסמא"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordWarning": MessageLookupByLibrary.simpleMessage( "אנחנו לא שומרים את הסיסמא הזו, לכן אם אתה שוכח אותה, אנחנו לא יכולים לפענח את המידע שלך"), "paymentDetails": MessageLookupByLibrary.simpleMessage("פרטי תשלום"), "paymentFailed": MessageLookupByLibrary.simpleMessage("התשלום נכשל"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage("אנשים משתמשים בקוד שלך"), "permanentlyDelete": @@ -645,7 +645,7 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("צור ticket"), "rateTheApp": MessageLookupByLibrary.simpleMessage("דרג את האפליקציה"), "rateUs": MessageLookupByLibrary.simpleMessage("דרג אותנו"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "recover": MessageLookupByLibrary.simpleMessage("שחזר"), "recoverAccount": MessageLookupByLibrary.simpleMessage("שחזר חשבון"), "recoverButton": MessageLookupByLibrary.simpleMessage("שחזר"), @@ -671,7 +671,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. תמסור את הקוד הזה לחברייך"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. הם נרשמים עבור תוכנית בתשלום"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("הפניות"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("הפניות כרגע מושהות"), @@ -687,7 +687,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("הסר מהאלבום?"), "removeLink": MessageLookupByLibrary.simpleMessage("הסרת קישור"), "removeParticipant": MessageLookupByLibrary.simpleMessage("הסר משתתף"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePublicLink": MessageLookupByLibrary.simpleMessage("הסר לינק ציבורי"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -738,8 +738,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedFoldersWillBeEncryptedAndBackedUp": MessageLookupByLibrary.simpleMessage( "התיקיות שנבחרו יוצפנו ויגובו"), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "send": MessageLookupByLibrary.simpleMessage("שלח"), "sendEmail": MessageLookupByLibrary.simpleMessage("שלח דוא\"ל"), "sendInvite": MessageLookupByLibrary.simpleMessage("שלח הזמנה"), @@ -758,15 +758,15 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("שתף אלבום עכשיו"), "shareLink": MessageLookupByLibrary.simpleMessage("שתף קישור"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage("שתף רק אם אנשים שאתה בוחר"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "הורד את ente על מנת שנוכל לשתף תמונות וסרטונים באיכות המקור באופן קל\n\nhttps://ente.io"), "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "שתף עם משתמשים שהם לא של ente"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("שתף את האלבום הראשון שלך"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -777,13 +777,13 @@ class MessageLookup extends MessageLookupByLibrary { "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "קבל התראות כשמישהו מוסיף תמונה לאלבום משותף שאתה חלק ממנו"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("שותף איתי"), "sharing": MessageLookupByLibrary.simpleMessage("משתף..."), "showMemories": MessageLookupByLibrary.simpleMessage("הצג זכרונות"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "אני מסכים לתנאי שירות ולמדיניות הפרטיות"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage("זה יימחק מכל האלבומים."), "skip": MessageLookupByLibrary.simpleMessage("דלג"), @@ -812,18 +812,18 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("אחסון"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("משפחה"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("אתה"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("גבול מקום האחסון נחרג"), "strongStrength": MessageLookupByLibrary.simpleMessage("חזקה"), - "subWillBeCancelledOn": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("הרשם"), "subscription": MessageLookupByLibrary.simpleMessage("מנוי"), "success": MessageLookupByLibrary.simpleMessage("הצלחה"), "suggestFeatures": MessageLookupByLibrary.simpleMessage("הציעו מאפיינים"), "support": MessageLookupByLibrary.simpleMessage("תמיכה"), - "syncProgress": m94, + "syncProgress": m95, "syncing": MessageLookupByLibrary.simpleMessage("מסנכרן..."), "systemTheme": MessageLookupByLibrary.simpleMessage("מערכת"), "tapToCopy": MessageLookupByLibrary.simpleMessage("הקש כדי להעתיק"), @@ -839,12 +839,12 @@ class MessageLookup extends MessageLookupByLibrary { "theDownloadCouldNotBeCompleted": MessageLookupByLibrary.simpleMessage("לא ניתן להשלים את ההורדה"), "theme": MessageLookupByLibrary.simpleMessage("ערכת נושא"), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "thisCanBeUsedToRecoverYourAccountIfYou": MessageLookupByLibrary.simpleMessage( "זה יכול לשמש לשחזור החשבון שלך במקרה ותאבד את הגורם השני"), "thisDevice": MessageLookupByLibrary.simpleMessage("מכשיר זה"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("זה מזהה האימות שלך"), "thisWillLogYouOutOfTheFollowingDevice": @@ -888,7 +888,7 @@ class MessageLookup extends MessageLookupByLibrary { "verificationId": MessageLookupByLibrary.simpleMessage("מזהה אימות"), "verify": MessageLookupByLibrary.simpleMessage("אמת"), "verifyEmail": MessageLookupByLibrary.simpleMessage("אימות דוא\"ל"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("אמת"), "verifyPassword": MessageLookupByLibrary.simpleMessage("אמת סיסמא"), "verifyingRecoveryKey": @@ -905,11 +905,11 @@ class MessageLookup extends MessageLookupByLibrary { "אנא בקר ב-web.ente.io על מנת לנהל את המנוי שלך"), "weAreOpenSource": MessageLookupByLibrary.simpleMessage("הקוד שלנו פתוח!"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("חלשה"), "welcomeBack": MessageLookupByLibrary.simpleMessage("ברוך שובך!"), "yearly": MessageLookupByLibrary.simpleMessage("שנתי"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("כן"), "yesCancel": MessageLookupByLibrary.simpleMessage("כן, בטל"), "yesConvertToViewer": @@ -932,7 +932,7 @@ class MessageLookup extends MessageLookupByLibrary { "אתה לא יכול לשנמך לתוכנית הזו"), "youCannotShareWithYourself": MessageLookupByLibrary.simpleMessage("אתה לא יכול לשתף עם עצמך"), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("החשבון שלך נמחק"), "yourPlanWasSuccessfullyDowngraded": diff --git a/mobile/lib/generated/intl/messages_id.dart b/mobile/lib/generated/intl/messages_id.dart index a26ef9c038..d3c1ea84ca 100644 --- a/mobile/lib/generated/intl/messages_id.dart +++ b/mobile/lib/generated/intl/messages_id.dart @@ -74,117 +74,117 @@ class MessageLookup extends MessageLookupByLibrary { static String m25(count, storageSaved) => "Kamu telah menghapus ${Intl.plural(count, other: '${count} file duplikat')} dan membersihkan (${storageSaved}!)"; - static String m27(newEmail) => "Email diubah menjadi ${newEmail}"; + static String m28(newEmail) => "Email diubah menjadi ${newEmail}"; - static String m29(email) => + static String m30(email) => "${email} tidak punya akun Ente.\n\nUndang dia untuk berbagi foto."; - static String m33(count, formattedNumber) => + static String m34(count, formattedNumber) => "${Intl.plural(count, other: '${formattedNumber} file')} di perangkat ini telah berhasil dicadangkan"; - static String m34(count, formattedNumber) => + static String m35(count, formattedNumber) => "${Intl.plural(count, other: '${formattedNumber} file')} dalam album ini telah berhasil dicadangkan"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB setiap kali orang mendaftar dengan paket berbayar lalu menerapkan kode milikmu"; - static String m36(endDate) => "Percobaan gratis berlaku hingga ${endDate}"; + static String m37(endDate) => "Percobaan gratis berlaku hingga ${endDate}"; - static String m38(sizeInMBorGB) => "Bersihkan ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Bersihkan ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Memproses ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => "${Intl.plural(count, other: '${count} item')}"; + static String m43(count) => "${Intl.plural(count, other: '${count} item')}"; - static String m45(expiryTime) => "Link akan kedaluwarsa pada ${expiryTime}"; + static String m46(expiryTime) => "Link akan kedaluwarsa pada ${expiryTime}"; - static String m50(albumName) => "Berhasil dipindahkan ke ${albumName}"; + static String m51(albumName) => "Berhasil dipindahkan ke ${albumName}"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Harap hubungi ${familyAdminEmail} untuk mengubah kode kamu."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Keamanan sandi: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Harap hubungi dukungan ${providerName} jika kamu dikenai biaya"; - static String m61(endDate) => + static String m62(endDate) => "Percobaan gratis berlaku hingga ${endDate}.\nKamu dapat memilih paket berbayar setelahnya."; - static String m62(toEmail) => "Silakan kirimi kami email di ${toEmail}"; + static String m63(toEmail) => "Silakan kirimi kami email di ${toEmail}"; - static String m63(toEmail) => "Silakan kirim log-nya ke \n${toEmail}"; + static String m64(toEmail) => "Silakan kirim log-nya ke \n${toEmail}"; - static String m66(storeName) => "Beri nilai di ${storeName}"; + static String m67(storeName) => "Beri nilai di ${storeName}"; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Kalian berdua mendapat ${storageInGB} GB* gratis"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} akan dikeluarkan dari album berbagi ini\n\nSemua foto yang ia tambahkan juga akan dihapus dari album ini"; - static String m73(endDate) => "Langganan akan diperpanjang pada ${endDate}"; + static String m74(endDate) => "Langganan akan diperpanjang pada ${endDate}"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, other: '${count} hasil ditemukan')}"; - static String m77(count) => "${count} terpilih"; + static String m78(count) => "${count} terpilih"; - static String m78(count, yourCount) => + static String m79(count, yourCount) => "${count} dipilih (${yourCount} milikmu)"; - static String m80(verificationID) => + static String m81(verificationID) => "Ini ID Verifikasi saya di ente.io: ${verificationID}."; - static String m81(verificationID) => + static String m82(verificationID) => "Halo, bisakah kamu pastikan bahwa ini adalah ID Verifikasi ente.io milikmu: ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Kode rujukan Ente: ${referralCode} \n\nTerapkan pada Pengaturan → Umum → Rujukan untuk mendapatkan ${referralStorageInGB} GB gratis setelah kamu mendaftar paket berbayar\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Bagikan dengan orang tertentu', one: 'Berbagi dengan 1 orang', other: 'Berbagi dengan ${numberOfPeople} orang')}"; - static String m84(emailIDs) => "Dibagikan dengan ${emailIDs}"; - - static String m85(fileType) => - "${fileType} ini akan dihapus dari perangkat ini."; + static String m85(emailIDs) => "Dibagikan dengan ${emailIDs}"; static String m86(fileType) => + "${fileType} ini akan dihapus dari perangkat ini."; + + static String m87(fileType) => "${fileType} ini tersimpan di Ente dan juga di perangkat ini."; - static String m87(fileType) => "${fileType} ini akan dihapus dari Ente."; + static String m88(fileType) => "${fileType} ini akan dihapus dari Ente."; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} dari ${totalAmount} ${totalStorageUnit} terpakai"; - static String m92(id) => + static String m93(id) => "${id} kamu telah terhubung dengan akun Ente lain.\nJika kamu ingin menggunakan ${id} kamu untuk akun ini, silahkan hubungi tim bantuan kami"; - static String m93(endDate) => + static String m94(endDate) => "Langganan kamu akan dibatalkan pada ${endDate}"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "Ia juga mendapat ${storageAmountInGB} GB"; - static String m97(email) => "Ini adalah ID Verifikasi milik ${email}"; + static String m98(email) => "Ini adalah ID Verifikasi milik ${email}"; - static String m107(endDate) => "Berlaku hingga ${endDate}"; + static String m108(endDate) => "Berlaku hingga ${endDate}"; - static String m108(email) => "Verifikasi ${email}"; + static String m109(email) => "Verifikasi ${email}"; - static String m110(email) => + static String m112(email) => "Kami telah mengirimkan email ke ${email}"; - static String m111(count) => + static String m113(count) => "${Intl.plural(count, other: '${count} tahun lalu')}"; - static String m113(storageSaved) => + static String m115(storageSaved) => "Kamu telah berhasil membersihkan ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -587,8 +587,8 @@ class MessageLookup extends MessageLookupByLibrary { "Perubahan lokasi hanya akan terlihat di Ente"), "eligible": MessageLookupByLibrary.simpleMessage("memenuhi syarat"), "email": MessageLookupByLibrary.simpleMessage("Email"), - "emailChangedTo": m27, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailNoEnteAccount": m30, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Verifikasi email"), "empty": MessageLookupByLibrary.simpleMessage("Kosongkan"), @@ -686,8 +686,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Jenis file"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Nama dan jenis file"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("File terhapus"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("File tersimpan ke galeri"), @@ -701,12 +701,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Wajah yang ditemukan"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Kuota gratis diperoleh"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Kuota gratis yang dapat digunakan"), "freeTrial": MessageLookupByLibrary.simpleMessage("Percobaan gratis"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Bersihkan penyimpanan perangkat"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -715,7 +715,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("Umum"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Menghasilkan kunci enkripsi..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Buka pengaturan"), "googlePlayId": MessageLookupByLibrary.simpleMessage("ID Google Play"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( @@ -775,7 +775,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Sepertinya terjadi kesalahan. Silakan coba lagi setelah beberapa saat. Jika kesalahan terus terjadi, silakan hubungi tim dukungan kami."), - "itemCount": m42, + "itemCount": m43, "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Item yang dipilih akan dihapus dari album ini"), "joinDiscord": @@ -802,7 +802,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Batas perangkat"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktif"), "linkExpired": MessageLookupByLibrary.simpleMessage("Kedaluwarsa"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Waktu kedaluwarsa link"), "linkHasExpired": @@ -883,7 +883,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Pindahkan ke album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Pindahkan ke album tersembunyi"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Pindah ke sampah"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -934,7 +934,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Di perangkat ini"), "onEnte": MessageLookupByLibrary.simpleMessage( "Di ente"), - "onlyFamilyAdminCanChangeCode": m53, + "onlyFamilyAdminCanChangeCode": m54, "oops": MessageLookupByLibrary.simpleMessage("Aduh"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( "Aduh, tidak dapat menyimpan perubahan"), @@ -961,7 +961,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sandi berhasil diubah"), "passwordLock": MessageLookupByLibrary.simpleMessage("Kunci dengan sandi"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordWarning": MessageLookupByLibrary.simpleMessage( "Kami tidak menyimpan sandi ini, jadi jika kamu melupakannya, kami tidak akan bisa mendekripsi data kamu"), "paymentDetails": @@ -970,7 +970,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Pembayaran gagal"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Sayangnya, pembayaranmu gagal. Silakan hubungi tim bantuan agar dapat kami bantu!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Item menunggu"), "pendingSync": MessageLookupByLibrary.simpleMessage("Sinkronisasi tertunda"), @@ -993,7 +993,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Foto yang telah kamu tambahkan akan dihapus dari album ini"), "playOnTv": MessageLookupByLibrary.simpleMessage("Putar album di TV"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Langganan PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1005,12 +1005,12 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Silakan hubungi tim bantuan jika masalah terus terjadi"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Harap berikan izin"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Silakan masuk akun lagi"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Silakan coba lagi"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1044,7 +1044,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Buat tiket dukungan"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Nilai app ini"), "rateUs": MessageLookupByLibrary.simpleMessage("Beri kami nilai"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "recover": MessageLookupByLibrary.simpleMessage("Pulihkan"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Pulihkan akun"), "recoverButton": MessageLookupByLibrary.simpleMessage("Pulihkan"), @@ -1072,7 +1072,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Berikan kode ini ke teman kamu"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Ia perlu daftar ke paket berbayar"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Referensi"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("Rujukan sedang dijeda"), @@ -1094,7 +1094,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Hapus link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Hapus peserta"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Hapus label orang"), "removePublicLink": @@ -1110,7 +1110,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Ubah nama file"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Perpanjang langganan"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Laporkan bug"), "reportBug": MessageLookupByLibrary.simpleMessage("Laporkan bug"), "resendEmail": @@ -1161,7 +1161,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Album, nama dan jenis file"), "searchHint5": MessageLookupByLibrary.simpleMessage( "Segera tiba: Penelusuran wajah & ajaib ✨"), - "searchResultCount": m75, + "searchResultCount": m76, "security": MessageLookupByLibrary.simpleMessage("Keamanan"), "selectALocation": MessageLookupByLibrary.simpleMessage("Pilih lokasi"), "selectALocationFirst": MessageLookupByLibrary.simpleMessage( @@ -1186,8 +1186,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Item terpilih akan dihapus dari semua album dan dipindahkan ke sampah."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "send": MessageLookupByLibrary.simpleMessage("Kirim"), "sendEmail": MessageLookupByLibrary.simpleMessage("Kirim email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Kirim undangan"), @@ -1208,16 +1208,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Bagikan album sekarang"), "shareLink": MessageLookupByLibrary.simpleMessage("Bagikan link"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Bagikan hanya dengan orang yang kamu inginkan"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Unduh Ente agar kita bisa berbagi foto dan video kualitas asli dengan mudah\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Bagikan ke pengguna non-Ente"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Bagikan album pertamamu"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1230,7 +1230,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Foto terbagi baru"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Terima notifikasi apabila seseorang menambahkan foto ke album bersama yang kamu ikuti"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Dibagikan dengan saya"), "sharedWithYou": @@ -1245,11 +1245,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Keluar di perangkat lain"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Saya menyetujui ketentuan layanan dan kebijakan privasi Ente"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Ia akan dihapus dari semua album."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Lewati"), "social": MessageLookupByLibrary.simpleMessage("Sosial"), "someItemsAreInBothEnteAndYourDevice": @@ -1291,13 +1291,13 @@ class MessageLookup extends MessageLookupByLibrary { "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Keluarga"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Kamu"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Batas penyimpanan terlampaui"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("Kuat"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Berlangganan"), "subscription": MessageLookupByLibrary.simpleMessage("Langganan"), "success": MessageLookupByLibrary.simpleMessage("Berhasil"), @@ -1337,7 +1337,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Item ini akan dihapus dari perangkat ini."), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( "Tindakan ini tidak dapat dibatalkan"), "thisAlbumAlreadyHDACollaborativeLink": @@ -1351,7 +1351,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Email ini telah digunakan"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Gambar ini tidak memiliki data exif"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Ini adalah ID Verifikasi kamu"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1417,14 +1417,14 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Gunakan kunci pemulihan"), "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Gunakan foto terpilih"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verifikasi gagal, silakan coba lagi"), "verificationId": MessageLookupByLibrary.simpleMessage("ID Verifikasi"), "verify": MessageLookupByLibrary.simpleMessage("Verifikasi"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verifikasi email"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verifikasi passkey"), "verifyPassword": @@ -1454,13 +1454,13 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Menunggu WiFi..."), "weAreOpenSource": MessageLookupByLibrary.simpleMessage("Kode sumber kami terbuka!"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Lemah"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Selamat datang kembali!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Hal yang baru"), "yearly": MessageLookupByLibrary.simpleMessage("Tahunan"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Ya"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ya, batalkan"), "yesConvertToViewer": @@ -1487,7 +1487,7 @@ class MessageLookup extends MessageLookupByLibrary { "Kamu tidak bisa berbagi dengan dirimu sendiri"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Kamu tidak memiliki item di arsip."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Akunmu telah dihapus"), "yourMap": MessageLookupByLibrary.simpleMessage("Peta kamu"), diff --git a/mobile/lib/generated/intl/messages_it.dart b/mobile/lib/generated/intl/messages_it.dart index 3e1bedb7af..06cf6c8925 100644 --- a/mobile/lib/generated/intl/messages_it.dart +++ b/mobile/lib/generated/intl/messages_it.dart @@ -85,158 +85,160 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} file, ${formattedSize} l\'uno"; - static String m27(newEmail) => "Email cambiata in ${newEmail}"; + static String m28(newEmail) => "Email cambiata in ${newEmail}"; - static String m29(email) => + static String m29(email) => "${email} non ha un account Ente."; + + static String m30(email) => "${email} non ha un account Ente.\n\nInvia un invito per condividere foto."; - static String m31(text) => "Trovate foto aggiuntive per ${text}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 file', other: '${formattedNumber} file')} di quest\'album sono stati salvati in modo sicuro"; + static String m32(text) => "Trovate foto aggiuntive per ${text}"; static String m34(count, formattedNumber) => "${Intl.plural(count, one: '1 file', other: '${formattedNumber} file')} di quest\'album sono stati salvati in modo sicuro"; - static String m35(storageAmountInGB) => + static String m35(count, formattedNumber) => + "${Intl.plural(count, one: '1 file', other: '${formattedNumber} file')} di quest\'album sono stati salvati in modo sicuro"; + + static String m36(storageAmountInGB) => "${storageAmountInGB} GB ogni volta che qualcuno si iscrive a un piano a pagamento e applica il tuo codice"; - static String m36(endDate) => "La prova gratuita termina il ${endDate}"; + static String m37(endDate) => "La prova gratuita termina il ${endDate}"; - static String m38(sizeInMBorGB) => "Libera ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Libera ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Elaborazione ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} elemento', other: '${count} elementi')}"; - static String m44(email) => + static String m45(email) => "${email} ti ha invitato a essere un contatto fidato"; - static String m45(expiryTime) => "Il link scadrà il ${expiryTime}"; + static String m46(expiryTime) => "Il link scadrà il ${expiryTime}"; - static String m46(email) => "Collega persona a ${email}"; + static String m47(email) => "Collega persona a ${email}"; - static String m50(albumName) => "Spostato con successo su ${albumName}"; + static String m51(albumName) => "Spostato con successo su ${albumName}"; - static String m51(personName) => "Nessun suggerimento per ${personName}"; + static String m52(personName) => "Nessun suggerimento per ${personName}"; - static String m52(name) => "Non è ${name}?"; + static String m53(name) => "Non è ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Per favore contatta ${familyAdminEmail} per cambiare il tuo codice."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Sicurezza password: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Si prega di parlare con il supporto di ${providerName} se ti è stato addebitato qualcosa"; - static String m61(endDate) => + static String m62(endDate) => "Prova gratuita valida fino al ${endDate}.\nIn seguito potrai scegliere un piano a pagamento."; - static String m62(toEmail) => "Per favore invia un\'email a ${toEmail}"; + static String m63(toEmail) => "Per favore invia un\'email a ${toEmail}"; - static String m63(toEmail) => "Invia i log a \n${toEmail}"; + static String m64(toEmail) => "Invia i log a \n${toEmail}"; - static String m65(folderName) => "Elaborando ${folderName}..."; + static String m66(folderName) => "Elaborando ${folderName}..."; - static String m66(storeName) => "Valutaci su ${storeName}"; + static String m67(storeName) => "Valutaci su ${storeName}"; - static String m67(name) => "Riassegnato a ${name}"; + static String m68(name) => "Riassegnato a ${name}"; - static String m68(days, email) => + static String m69(days, email) => "Puoi accedere all\'account dopo ${days} giorni. Una notifica verrà inviata a ${email}."; - static String m69(email) => + static String m70(email) => "Ora puoi recuperare l\'account di ${email} impostando una nuova password."; - static String m70(email) => + static String m71(email) => "${email} sta cercando di recuperare il tuo account."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Ottenete entrambi ${storageInGB} GB* gratis"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} verrà rimosso da questo album condiviso\n\nQualsiasi foto aggiunta dall\'utente verrà rimossa dall\'album"; - static String m73(endDate) => "Si rinnova il ${endDate}"; + static String m74(endDate) => "Si rinnova il ${endDate}"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} risultato trovato', other: '${count} risultati trovati')}"; - static String m77(count) => "${count} selezionati"; + static String m78(count) => "${count} selezionati"; - static String m78(count, yourCount) => + static String m79(count, yourCount) => "${count} selezionato (${yourCount} tuoi)"; - static String m80(verificationID) => + static String m81(verificationID) => "Ecco il mio ID di verifica: ${verificationID} per ente.io."; - static String m81(verificationID) => + static String m82(verificationID) => "Hey, puoi confermare che questo è il tuo ID di verifica: ${verificationID} su ente.io"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Codice invito Ente: ${referralCode} \n\nInseriscilo in Impostazioni → Generali → Inviti per ottenere ${referralStorageInGB} GB gratis dopo la sottoscrizione a un piano a pagamento\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Condividi con persone specifiche', one: 'Condividi con una persona', other: 'Condividi con ${numberOfPeople} persone')}"; - static String m84(emailIDs) => "Condiviso con ${emailIDs}"; - - static String m85(fileType) => - "Questo ${fileType} verrà eliminato dal tuo dispositivo."; + static String m85(emailIDs) => "Condiviso con ${emailIDs}"; static String m86(fileType) => + "Questo ${fileType} verrà eliminato dal tuo dispositivo."; + + static String m87(fileType) => "Questo ${fileType} è sia su Ente che sul tuo dispositivo."; - static String m87(fileType) => "Questo ${fileType} verrà eliminato da Ente."; + static String m88(fileType) => "Questo ${fileType} verrà eliminato da Ente."; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} di ${totalAmount} ${totalStorageUnit} utilizzati"; - static String m92(id) => + static String m93(id) => "Il tuo ${id} è già collegato a un altro account Ente.\nSe desideri utilizzare il tuo ${id} con questo account, per favore contatta il nostro supporto\'\'"; - static String m93(endDate) => "L\'abbonamento verrà cancellato il ${endDate}"; + static String m94(endDate) => "L\'abbonamento verrà cancellato il ${endDate}"; - static String m94(completed, total) => + static String m95(completed, total) => "${completed}/${total} ricordi conservati"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "Tocca per caricare, il caricamento è attualmente ignorato a causa di ${ignoreReason}"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "Anche loro riceveranno ${storageAmountInGB} GB"; - static String m97(email) => "Questo è l\'ID di verifica di ${email}"; + static String m98(email) => "Questo è l\'ID di verifica di ${email}"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Presto', one: '1 giorno', other: '${count} giorni')}"; - static String m103(email) => + static String m104(email) => "Sei stato invitato a essere un contatto Legacy da ${email}."; - static String m105(ignoreReason) => + static String m106(ignoreReason) => "Il caricamento è ignorato a causa di ${ignoreReason}"; - static String m106(count) => "Conservando ${count} ricordi..."; + static String m107(count) => "Conservando ${count} ricordi..."; - static String m107(endDate) => "Valido fino al ${endDate}"; + static String m108(endDate) => "Valido fino al ${endDate}"; - static String m108(email) => "Verifica ${email}"; + static String m109(email) => "Verifica ${email}"; - static String m110(email) => + static String m112(email) => "Abbiamo inviato una mail a ${email}"; - static String m111(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} anno fa', other: '${count} anni fa')}"; - static String m113(storageSaved) => + static String m115(storageSaved) => "Hai liberato con successo ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -774,8 +776,9 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("Email"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("Email già registrata."), - "emailChangedTo": m27, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("Email non registrata."), "emailVerificationToggle": @@ -857,7 +860,7 @@ class MessageLookup extends MessageLookupByLibrary { "exportYourData": MessageLookupByLibrary.simpleMessage("Esporta dati"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Trovate foto aggiuntive"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Faccia non ancora raggruppata, per favore torna più tardi"), "faceRecognition": @@ -907,8 +910,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Tipi di file"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Tipi e nomi di file"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("File eliminati"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("File salvati nella galleria"), @@ -924,12 +927,12 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Volti trovati"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Spazio gratuito richiesto"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Spazio libero utilizzabile"), "freeTrial": MessageLookupByLibrary.simpleMessage("Prova gratuita"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Libera spazio"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -941,7 +944,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("Generali"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generazione delle chiavi di crittografia..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Vai alle impostazioni"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), @@ -1026,12 +1029,14 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Sembra che qualcosa sia andato storto. Riprova tra un po\'. Se l\'errore persiste, contatta il nostro team di supporto."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Gli elementi mostrano il numero di giorni rimanenti prima della cancellazione permanente"), "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Gli elementi selezionati saranno rimossi da questo album"), + "joinAlbumConfirmationDialogBody": MessageLookupByLibrary.simpleMessage( + "Unirsi a un album renderà visibile la tua email ai suoi partecipanti."), "joinDiscord": MessageLookupByLibrary.simpleMessage("Unisciti a Discord"), "keepPhotos": MessageLookupByLibrary.simpleMessage("Mantieni foto"), @@ -1052,7 +1057,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Legacy"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Account Legacy"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Legacy consente ai contatti fidati di accedere al tuo account in tua assenza."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1067,7 +1072,7 @@ class MessageLookup extends MessageLookupByLibrary { "per una condivisione più veloce"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Attivato"), "linkExpired": MessageLookupByLibrary.simpleMessage("Scaduto"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Scadenza del link"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Il link è scaduto"), @@ -1075,10 +1080,12 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Collega persona"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "per una migliore esperienza di condivisione"), - "linkPersonToEmail": m46, + "linkPersonToEmail": m47, "livePhotos": MessageLookupByLibrary.simpleMessage("Live Photo"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Puoi condividere il tuo abbonamento con la tua famiglia"), + "loadMessage2": MessageLookupByLibrary.simpleMessage( + "Finora abbiamo conservato oltre 200 milioni di ricordi"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Teniamo 3 copie dei tuoi dati, uno in un rifugio sotterraneo antiatomico"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1195,7 +1202,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sposta nell\'album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Sposta in album nascosto"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Spostato nel cestino"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1248,10 +1255,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Nessun risultato"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Nessun risultato trovato"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Nessun blocco di sistema trovato"), - "notPersonLabel": m52, + "notPersonLabel": m53, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Ancora nulla di condiviso con te"), "nothingToSeeHere": @@ -1261,7 +1268,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Sul dispositivo"), "onEnte": MessageLookupByLibrary.simpleMessage( "Su ente"), - "onlyFamilyAdminCanChangeCode": m53, + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Solo loro"), "oops": MessageLookupByLibrary.simpleMessage("Oops"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1300,7 +1307,7 @@ class MessageLookup extends MessageLookupByLibrary { "Password modificata con successo"), "passwordLock": MessageLookupByLibrary.simpleMessage("Blocco con password"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "La sicurezza della password viene calcolata considerando la lunghezza della password, i caratteri usati e se la password appare o meno nelle prime 10.000 password più usate"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1311,7 +1318,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Pagamento non riuscito"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Purtroppo il tuo pagamento non è riuscito. Contatta l\'assistenza e ti aiuteremo!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Elementi in sospeso"), "pendingSync": @@ -1342,7 +1349,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("Blocco con PIN"), "playOnTv": MessageLookupByLibrary.simpleMessage("Riproduci album sulla TV"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Abbonamento su PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1354,14 +1361,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Riprova. Se il problema persiste, ti invitiamo a contattare l\'assistenza"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Concedi i permessi"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage( "Effettua nuovamente l\'accesso"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Si prega di selezionare i link rapidi da rimuovere"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Riprova"), "pleaseVerifyTheCodeYouHaveEntered": MessageLookupByLibrary.simpleMessage( @@ -1386,7 +1393,7 @@ class MessageLookup extends MessageLookupByLibrary { "privateSharing": MessageLookupByLibrary.simpleMessage("Condivisioni private"), "proceed": MessageLookupByLibrary.simpleMessage("Prosegui"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Elaborando video"), "publicLinkCreated": @@ -1399,9 +1406,9 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Invia ticket"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Valuta l\'app"), "rateUs": MessageLookupByLibrary.simpleMessage("Lascia una recensione"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("Riassegna \"Io\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Riassegnando..."), "recover": MessageLookupByLibrary.simpleMessage("Recupera"), @@ -1412,7 +1419,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recupera l\'account"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Recupero avviato"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Chiave di recupero"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1427,12 +1434,12 @@ class MessageLookup extends MessageLookupByLibrary { "Chiave di recupero verificata"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Se hai dimenticato la password, la tua chiave di ripristino è l\'unico modo per recuperare le tue foto. La puoi trovare in Impostazioni > Account.\n\nInserisci la tua chiave di recupero per verificare di averla salvata correttamente."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Recupero riuscito!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Un contatto fidato sta tentando di accedere al tuo account"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Il dispositivo attuale non è abbastanza potente per verificare la tua password, ma la possiamo rigenerare in un modo che funzioni su tutti i dispositivi.\n\nEffettua il login utilizzando la tua chiave di recupero e rigenera la tua password (puoi utilizzare nuovamente la stessa se vuoi)."), "recreatePasswordTitle": @@ -1448,7 +1455,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Condividi questo codice con i tuoi amici"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Si iscrivono per un piano a pagamento"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Invita un Amico"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "I referral code sono attualmente in pausa"), @@ -1477,7 +1484,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Elimina link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Rimuovi partecipante"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Rimuovi etichetta persona"), "removePublicLink": @@ -1497,7 +1504,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Rinomina file"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Rinnova abbonamento"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Segnala un bug"), "reportBug": MessageLookupByLibrary.simpleMessage("Segnala un bug"), "resendEmail": MessageLookupByLibrary.simpleMessage("Rinvia email"), @@ -1574,7 +1581,7 @@ class MessageLookup extends MessageLookupByLibrary { "Invita persone e vedrai qui tutte le foto condivise da loro"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Le persone saranno mostrate qui una volta che l\'elaborazione e la sincronizzazione saranno completate"), - "searchResultCount": m75, + "searchResultCount": m76, "security": MessageLookupByLibrary.simpleMessage("Sicurezza"), "selectALocation": MessageLookupByLibrary.simpleMessage("Seleziona un luogo"), @@ -1606,8 +1613,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Gli elementi selezionati verranno eliminati da tutti gli album e spostati nel cestino."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "send": MessageLookupByLibrary.simpleMessage("Invia"), "sendEmail": MessageLookupByLibrary.simpleMessage("Invia email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Invita"), @@ -1639,16 +1646,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Condividi un album"), "shareLink": MessageLookupByLibrary.simpleMessage("Condividi link"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Condividi solo con le persone che vuoi"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Scarica Ente in modo da poter facilmente condividere foto e video in qualità originale\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Condividi con utenti che non hanno un account Ente"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Condividi il tuo primo album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1659,7 +1666,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nuove foto condivise"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Ricevi notifiche quando qualcuno aggiunge una foto a un album condiviso, di cui fai parte"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Condivisi con me"), "sharedWithYou": @@ -1676,11 +1683,11 @@ class MessageLookup extends MessageLookupByLibrary { "Esci dagli altri dispositivi"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Accetto i termini di servizio e la politica sulla privacy"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Verrà eliminato da tutti gli album."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Salta"), "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": @@ -1729,13 +1736,13 @@ class MessageLookup extends MessageLookupByLibrary { "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Famiglia"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Tu"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Limite d\'archiviazione superato"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("Forte"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Iscriviti"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "È necessario un abbonamento a pagamento attivo per abilitare la condivisione."), @@ -1752,7 +1759,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Suggerisci una funzionalità"), "support": MessageLookupByLibrary.simpleMessage("Assistenza"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronizzazione interrotta"), "syncing": MessageLookupByLibrary.simpleMessage( @@ -1765,7 +1772,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Tocca per sbloccare"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Premi per caricare"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Sembra che qualcosa sia andato storto. Riprova tra un po\'. Se l\'errore persiste, contatta il nostro team di supporto."), "terminate": MessageLookupByLibrary.simpleMessage("Terminata"), @@ -1786,7 +1793,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Questi file verranno eliminati dal tuo dispositivo."), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Verranno eliminati da tutti gli album."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1805,7 +1812,7 @@ class MessageLookup extends MessageLookupByLibrary { "Questa immagine non ha dati EXIF"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Questo sono io!"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Questo è il tuo ID di verifica"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1829,11 +1836,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("totale"), "totalSize": MessageLookupByLibrary.simpleMessage("Dimensioni totali"), "trash": MessageLookupByLibrary.simpleMessage("Cestino"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Taglia"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Contatti fidati"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Riprova"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Attiva il backup per caricare automaticamente i file aggiunti a questa cartella del dispositivo su Ente."), @@ -1879,10 +1886,10 @@ class MessageLookup extends MessageLookupByLibrary { "Aggiornamento della selezione delle cartelle..."), "upgrade": MessageLookupByLibrary.simpleMessage("Acquista altro spazio"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Caricamento dei file nell\'album..."), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Conservando 1 ricordo..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1901,7 +1908,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Usa la foto selezionata"), "usedSpace": MessageLookupByLibrary.simpleMessage("Spazio utilizzato"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verifica fallita, per favore prova di nuovo"), @@ -1909,7 +1916,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ID di verifica"), "verify": MessageLookupByLibrary.simpleMessage("Verifica"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verifica email"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verifica"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verifica passkey"), @@ -1949,7 +1956,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Non puoi modificare foto e album che non possiedi"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Debole"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bentornato/a!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Novità"), @@ -1957,7 +1964,7 @@ class MessageLookup extends MessageLookupByLibrary { "Un contatto fidato può aiutare a recuperare i tuoi dati."), "yearShort": MessageLookupByLibrary.simpleMessage("anno"), "yearly": MessageLookupByLibrary.simpleMessage("Annuale"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Si"), "yesCancel": MessageLookupByLibrary.simpleMessage("Sì, cancella"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -1989,7 +1996,7 @@ class MessageLookup extends MessageLookupByLibrary { "Non puoi condividere con te stesso"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Non hai nulla di archiviato."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Il tuo account è stato eliminato"), "yourMap": MessageLookupByLibrary.simpleMessage("La tua mappa"), diff --git a/mobile/lib/generated/intl/messages_ja.dart b/mobile/lib/generated/intl/messages_ja.dart index 814fb0413f..b7fe3d8bca 100644 --- a/mobile/lib/generated/intl/messages_ja.dart +++ b/mobile/lib/generated/intl/messages_ja.dart @@ -87,191 +87,191 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} 個のファイル、それぞれ${formattedSize}"; - static String m27(newEmail) => "メールアドレスが ${newEmail} に変更されました"; + static String m28(newEmail) => "メールアドレスが ${newEmail} に変更されました"; - static String m28(email) => "${email} は Ente アカウントを持っていません。"; + static String m29(email) => "${email} は Ente アカウントを持っていません。"; - static String m29(email) => + static String m30(email) => "${email} はEnteアカウントを持っていません。\n\n写真を共有するために「招待」を送信してください。"; - static String m30(name) => "${name}抱きしめて!"; + static String m31(name) => "${name}抱きしめて!"; - static String m31(text) => "${text} の写真が見つかりました"; + static String m32(text) => "${text} の写真が見つかりました"; - static String m32(name) => "${name}とご飯!"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, other: '${formattedNumber} 個のファイル')} が安全にバックアップされました"; + static String m33(name) => "${name}とご飯!"; static String m34(count, formattedNumber) => + "${Intl.plural(count, other: '${formattedNumber} 個のファイル')} が安全にバックアップされました"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, other: '${formattedNumber} ファイル')} が安全にバックアップされました"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "誰かが有料プランにサインアップしてコードを適用する度に ${storageAmountInGB} GB"; - static String m36(endDate) => "無料トライアルは${endDate} までです"; + static String m37(endDate) => "無料トライアルは${endDate} までです"; - static String m38(sizeInMBorGB) => "${sizeInMBorGB} を解放する"; + static String m39(sizeInMBorGB) => "${sizeInMBorGB} を解放する"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "${currentlyProcessing} / ${totalCount} を処理中"; - static String m41(name) => "${name}とハイキング!"; + static String m42(name) => "${name}とハイキング!"; - static String m42(count) => "${Intl.plural(count, other: '${count}個のアイテム')}"; + static String m43(count) => "${Intl.plural(count, other: '${count}個のアイテム')}"; - static String m43(name) => "前回の${name}との時間"; + static String m44(name) => "前回の${name}との時間"; - static String m44(email) => "${email} があなたを信頼する連絡先として招待しました"; + static String m45(email) => "${email} があなたを信頼する連絡先として招待しました"; - static String m45(expiryTime) => "リンクは ${expiryTime} に期限切れになります"; + static String m46(expiryTime) => "リンクは ${expiryTime} に期限切れになります"; - static String m46(email) => "この人物を ${email}に紐づけ"; + static String m47(email) => "この人物を ${email}に紐づけ"; - static String m47(personName, email) => "${personName} を ${email} に紐づけします"; + static String m48(personName, email) => "${personName} を ${email} に紐づけします"; - static String m50(albumName) => "${albumName} に移動しました"; + static String m51(albumName) => "${albumName} に移動しました"; - static String m51(personName) => "${personName} の候補はありません"; + static String m52(personName) => "${personName} の候補はありません"; - static String m52(name) => "${name} ではありませんか?"; + static String m53(name) => "${name} ではありませんか?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "コードを変更するには、 ${familyAdminEmail} までご連絡ください。"; - static String m54(name) => "${name}とパーティー!"; + static String m55(name) => "${name}とパーティー!"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "パスワードの長さ: ${passwordStrengthValue}"; - static String m56(providerName) => "請求された場合は、 ${providerName} のサポートに連絡してください"; + static String m57(providerName) => "請求された場合は、 ${providerName} のサポートに連絡してください"; - static String m57(name, age) => "${name}が${age}才!"; + static String m58(name, age) => "${name}が${age}才!"; - static String m58(name, age) => "${name}が${age}才になった!"; + static String m59(name, age) => "${name}が${age}才になった!"; - static String m59(count) => + static String m60(count) => "${Intl.plural(count, zero: '0枚の写真', one: '1枚の写真', other: '${count} 枚の写真')}"; - static String m61(endDate) => + static String m62(endDate) => "${endDate} まで無料トライアルが有効です。\nその後、有料プランを選択することができます。"; - static String m62(toEmail) => "${toEmail} にメールでご連絡ください"; + static String m63(toEmail) => "${toEmail} にメールでご連絡ください"; - static String m63(toEmail) => "ログを以下のアドレスに送信してください \n${toEmail}"; + static String m64(toEmail) => "ログを以下のアドレスに送信してください \n${toEmail}"; - static String m64(name) => "${name}と一緒にポーズ!"; + static String m65(name) => "${name}と一緒にポーズ!"; - static String m65(folderName) => "${folderName} を処理中..."; + static String m66(folderName) => "${folderName} を処理中..."; - static String m66(storeName) => "${storeName} で評価"; + static String m67(storeName) => "${storeName} で評価"; - static String m67(name) => "あなたを ${name} に紐づけました"; + static String m68(name) => "あなたを ${name} に紐づけました"; - static String m68(days, email) => + static String m69(days, email) => "${days} 日後にアカウントにアクセスできます。通知は ${email}に送信されます。"; - static String m69(email) => "${email}のアカウントを復元できるようになりました。新しいパスワードを設定してください。"; + static String m70(email) => "${email}のアカウントを復元できるようになりました。新しいパスワードを設定してください。"; - static String m70(email) => "${email} はあなたのアカウントを復元しようとしています。"; + static String m71(email) => "${email} はあなたのアカウントを復元しようとしています。"; - static String m71(storageInGB) => "3. お二人とも ${storageInGB} GB*を無料で手に入ります。"; + static String m72(storageInGB) => "3. お二人とも ${storageInGB} GB*を無料で手に入ります。"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} はこの共有アルバムから退出します\n\n${userEmail} が追加した写真もアルバムから削除されます"; - static String m73(endDate) => "サブスクリプションは ${endDate} に更新します"; + static String m74(endDate) => "サブスクリプションは ${endDate} に更新します"; - static String m74(name) => "${name}と車で旅行!"; + static String m75(name) => "${name}と車で旅行!"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} 個の結果', other: '${count} 個の結果')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "セクションの長さの不一致: ${snapshotLength} != ${searchLength}"; - static String m77(count) => "${count} 個を選択"; + static String m78(count) => "${count} 個を選択"; - static String m78(count, yourCount) => "${count} 個選択中(${yourCount} あなた)"; + static String m79(count, yourCount) => "${count} 個選択中(${yourCount} あなた)"; - static String m79(name) => "${name}とセルフィー!"; + static String m80(name) => "${name}とセルフィー!"; - static String m80(verificationID) => "私の確認ID: ente.ioの ${verificationID}"; + static String m81(verificationID) => "私の確認ID: ente.ioの ${verificationID}"; - static String m81(verificationID) => + static String m82(verificationID) => "これがあなたのente.io確認用IDであることを確認できますか? ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "リフェラルコード: ${referralCode}\n\n設定→一般→リフェラルで使うことで${referralStorageInGB}が無料になります(あなたが有料プランに加入したあと)。\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: '誰かと共有しましょう', one: '1人と共有されています', other: '${numberOfPeople} 人と共有されています')}"; - static String m84(emailIDs) => "${emailIDs} と共有中"; + static String m85(emailIDs) => "${emailIDs} と共有中"; - static String m85(fileType) => "${fileType} はEnteから削除されます。"; + static String m86(fileType) => "${fileType} はEnteから削除されます。"; - static String m86(fileType) => "この ${fileType} はEnteとお使いのデバイスの両方にあります。"; + static String m87(fileType) => "この ${fileType} はEnteとお使いのデバイスの両方にあります。"; - static String m87(fileType) => "${fileType} はEnteから削除されます。"; + static String m88(fileType) => "${fileType} はEnteから削除されます。"; - static String m88(name) => "${name}とスポーツ!"; + static String m89(name) => "${name}とスポーツ!"; - static String m89(name) => "${name}にスポットライト!"; + static String m90(name) => "${name}にスポットライト!"; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} / ${totalAmount} ${totalStorageUnit} 使用"; - static String m92(id) => + static String m93(id) => "あなたの ${id} はすでに別のEnteアカウントにリンクされています。\nこのアカウントであなたの ${id} を使用したい場合は、サポートにお問い合わせください。"; - static String m93(endDate) => "サブスクリプションは ${endDate} でキャンセルされます"; + static String m94(endDate) => "サブスクリプションは ${endDate} でキャンセルされます"; - static String m94(completed, total) => "${completed}/${total} のメモリが保存されました"; + static String m95(completed, total) => "${completed}/${total} のメモリが保存されました"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "アップロードするにはタップしてください。 以下の理由のためアップロードは現在無視されています: ${ignoreReason}"; - static String m96(storageAmountInGB) => "紹介者も ${storageAmountInGB} GB を得ます"; + static String m97(storageAmountInGB) => "紹介者も ${storageAmountInGB} GB を得ます"; - static String m97(email) => "これは ${email} の確認用ID"; + static String m98(email) => "これは ${email} の確認用ID"; - static String m98(count) => + static String m99(count) => "${Intl.plural(count, one: '${count} 1年前の今週', other: '${count}年前の今週')}"; - static String m99(dateFormat) => "${dateFormat} から年"; + static String m100(dateFormat) => "${dateFormat} から年"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: '', one: '1日', other: '${count} 日')}"; - static String m101(year) => "${year}年の旅行"; + static String m102(year) => "${year}年の旅行"; - static String m102(location) => "${location}への旅行"; + static String m103(location) => "${location}への旅行"; - static String m103(email) => "あなたは ${email}から信頼する連絡先になってもらうよう、お願いされています。"; + static String m104(email) => "あなたは ${email}から信頼する連絡先になってもらうよう、お願いされています。"; - static String m104(galleryType) => + static String m105(galleryType) => "このギャラリーのタイプ ${galleryType} は名前の変更には対応していません"; - static String m105(ignoreReason) => "以下の理由によりアップロードは無視されます: ${ignoreReason}"; + static String m106(ignoreReason) => "以下の理由によりアップロードは無視されます: ${ignoreReason}"; - static String m106(count) => "${count} メモリを保存しています..."; + static String m107(count) => "${count} メモリを保存しています..."; - static String m107(endDate) => "${endDate} まで"; + static String m108(endDate) => "${endDate} まで"; - static String m108(email) => "${email} を確認"; + static String m109(email) => "${email} を確認"; - static String m110(email) => "${email}にメールを送りました"; + static String m112(email) => "${email}にメールを送りました"; - static String m111(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} 年前', other: '${count} 年前')}"; - static String m112(name) => "あなたと${name}"; + static String m114(name) => "あなたと${name}"; - static String m113(storageSaved) => "${storageSaved} を解放しました"; + static String m115(storageSaved) => "${storageSaved} を解放しました"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -461,21 +461,6 @@ class MessageLookup extends MessageLookupByLibrary { "birthday": MessageLookupByLibrary.simpleMessage("誕生日"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("ブラックフライデーセール"), "blog": MessageLookupByLibrary.simpleMessage("ブログ"), - "cLBulkEdit": MessageLookupByLibrary.simpleMessage("日付を一括編集"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "複数の写真を、1回の操作で日付/時刻を編集することもできるようになりました。"), - "cLFamilyPlan": MessageLookupByLibrary.simpleMessage("ファミリープランの制限"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "ファミリーメンバーが使用できるストレージ容量の制限を設定できるようになりました。"), - "cLIcon": MessageLookupByLibrary.simpleMessage("新しいアイコン"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "新しいアプリのアイコンが登場です!ちなみに、古いアイコンが好きだった人のためにアイコンの切り替え機能も用意がございます。"), - "cLMemories": MessageLookupByLibrary.simpleMessage("思い出"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "あなたにとって特別な瞬間を再発見しましょう。 - あなたの好きな人々、あなたの旅行や休日。 機械学習をオンにすると、自分自身にタグを付けたり、友達の顔に名前をつけたりすることができます。"), - "cLWidgets": MessageLookupByLibrary.simpleMessage("ウィジェット"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Enteの「思い出」機能と統合されたホーム画面ウィジェットが利用可能になりました!"), "cachedData": MessageLookupByLibrary.simpleMessage("キャッシュデータ"), "calculating": MessageLookupByLibrary.simpleMessage("計算中..."), "canNotOpenBody": MessageLookupByLibrary.simpleMessage( @@ -739,15 +724,15 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("Eメール"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("このメールアドレスはすでに登録されています。"), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("このメールアドレスはまだ登録されていません。"), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("メール確認"), "emailYourLogs": MessageLookupByLibrary.simpleMessage("ログをメールで送信"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("緊急連絡先"), "empty": MessageLookupByLibrary.simpleMessage("空"), "emptyTrash": MessageLookupByLibrary.simpleMessage("ゴミ箱を空にしますか?"), @@ -814,7 +799,7 @@ class MessageLookup extends MessageLookupByLibrary { "exportYourData": MessageLookupByLibrary.simpleMessage("データをエクスポート"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("追加の写真が見つかりました"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage("顔がまだ集まっていません。後で戻ってきてください"), "faceRecognition": MessageLookupByLibrary.simpleMessage("顔認識"), @@ -847,7 +832,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("よくある質問"), "faqs": MessageLookupByLibrary.simpleMessage("よくある質問"), "favorite": MessageLookupByLibrary.simpleMessage("お気に入り"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("フィードバック"), "file": MessageLookupByLibrary.simpleMessage("ファイル"), "fileFailedToSaveToGallery": @@ -859,8 +844,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ファイルをギャラリーに保存しました"), "fileTypes": MessageLookupByLibrary.simpleMessage("ファイルの種類"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("ファイルの種類と名前"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("削除されたファイル"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("写真をダウンロードしました"), @@ -872,12 +857,12 @@ class MessageLookup extends MessageLookupByLibrary { "forgotPassword": MessageLookupByLibrary.simpleMessage("パスワードを忘れた"), "foundFaces": MessageLookupByLibrary.simpleMessage("見つかった顔"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("空き容量を受け取る"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("無料のストレージが利用可能です"), "freeTrial": MessageLookupByLibrary.simpleMessage("無料トライアル"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("デバイスの空き領域を解放する"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -889,7 +874,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("設定"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage("暗号化鍵を生成しています"), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("設定に移動"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( @@ -916,7 +901,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage("ホームギャラリーから共有された写真等を非表示"), "hiding": MessageLookupByLibrary.simpleMessage("非表示にしています"), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("OSM Franceでホスト"), "howItWorks": MessageLookupByLibrary.simpleMessage("仕組みを知る"), @@ -967,7 +952,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "問題が発生したようです。しばらくしてから再試行してください。エラーが解決しない場合は、サポートチームにお問い合わせください。"), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage("完全に削除されるまでの日数が項目に表示されます"), "itemsWillBeRemovedFromAlbum": @@ -986,7 +971,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage("よければ、情報をお寄せください"), "language": MessageLookupByLibrary.simpleMessage("言語"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("更新された順"), "lastYearsTrip": MessageLookupByLibrary.simpleMessage("昨年の旅行"), "leave": MessageLookupByLibrary.simpleMessage("離脱"), @@ -997,7 +982,7 @@ class MessageLookup extends MessageLookupByLibrary { "left": MessageLookupByLibrary.simpleMessage("左"), "legacy": MessageLookupByLibrary.simpleMessage("レガシー"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("レガシーアカウント"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "レガシーでは、信頼できる連絡先が不在時(あなたが亡くなった時など)にアカウントにアクセスできます。"), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1013,15 +998,15 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("共有を高速化するために"), "linkEnabled": MessageLookupByLibrary.simpleMessage("有効"), "linkExpired": MessageLookupByLibrary.simpleMessage("期限切れ"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("リンクの期限切れ"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("リンクは期限切れです"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("なし"), "linkPerson": MessageLookupByLibrary.simpleMessage("人を紐づけ"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage("良い経験を分かち合うために"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("ライブフォト"), "loadMessage1": MessageLookupByLibrary.simpleMessage("サブスクリプションを家族と共有できます"), @@ -1129,7 +1114,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("選択した写真を1つの日付に移動"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("アルバムに移動"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("隠しアルバムに移動"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("ごみ箱へ移動"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage("アルバムにファイルを移動中"), @@ -1176,10 +1161,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("該当なし"), "noResultsFound": MessageLookupByLibrary.simpleMessage("一致する結果が見つかりませんでした"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("システムロックが見つかりませんでした"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("この人ではありませんか?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage("あなたに共有されたものはありません"), @@ -1191,7 +1176,7 @@ class MessageLookup extends MessageLookupByLibrary { "onEnte": MessageLookupByLibrary.simpleMessage( "Enteが保管"), "onTheRoad": MessageLookupByLibrary.simpleMessage("再び道で"), - "onlyFamilyAdminCanChangeCode": m53, + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("この人のみ"), "oops": MessageLookupByLibrary.simpleMessage("Oops"), "oopsCouldNotSaveEdits": @@ -1219,7 +1204,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairWithPin": MessageLookupByLibrary.simpleMessage("PINを使ってペアリングする"), "pairingComplete": MessageLookupByLibrary.simpleMessage("ペアリング完了"), "panorama": MessageLookupByLibrary.simpleMessage("パノラマ"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage("検証はまだ保留中です"), "passkey": MessageLookupByLibrary.simpleMessage("パスキー"), @@ -1228,7 +1213,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("パスワードの変更に成功しました"), "passwordLock": MessageLookupByLibrary.simpleMessage("パスワード保護"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "パスワードの長さ、使用される文字の種類を考慮してパスワードの強度は計算されます。"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1237,7 +1222,7 @@ class MessageLookup extends MessageLookupByLibrary { "paymentFailed": MessageLookupByLibrary.simpleMessage("支払いに失敗しました"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "残念ながらお支払いに失敗しました。サポートにお問い合わせください。お手伝いします!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("処理待ちの項目"), "pendingSync": MessageLookupByLibrary.simpleMessage("同期を保留中"), "people": MessageLookupByLibrary.simpleMessage("人物"), @@ -1248,14 +1233,14 @@ class MessageLookup extends MessageLookupByLibrary { "permanentlyDelete": MessageLookupByLibrary.simpleMessage("完全に削除"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage("デバイスから完全に削除しますか?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("人名名"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("毛むくじゃらな仲間たち"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("写真の説明"), "photoGridSize": MessageLookupByLibrary.simpleMessage("写真のグリッドサイズ"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("写真"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("写真"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage("あなたの追加した写真はこのアルバムから削除されます"), @@ -1266,7 +1251,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("PINロック"), "playOnTv": MessageLookupByLibrary.simpleMessage("TVでアルバムを再生"), "playOriginal": MessageLookupByLibrary.simpleMessage("元動画を再生"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("再生"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStoreサブスクリプション"), @@ -1277,13 +1262,13 @@ class MessageLookup extends MessageLookupByLibrary { "Support@ente.ioにお問い合わせください、お手伝いいたします。"), "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage("問題が解決しない場合はサポートにお問い合わせください"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("権限を付与してください"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("もう一度試してください"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage("削除するクイックリンクを選択してください"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("もう一度試してください"), "pleaseVerifyTheCodeYouHaveEntered": MessageLookupByLibrary.simpleMessage("入力したコードを確認してください"), @@ -1294,7 +1279,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("再試行する前にしばらくお待ちください"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage("しばらくお待ちください。時間がかかります。"), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("ログを準備中..."), "preserveMore": MessageLookupByLibrary.simpleMessage("もっと保存する"), "pressAndHoldToPlayVideo": @@ -1310,7 +1295,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("続行"), "processed": MessageLookupByLibrary.simpleMessage("処理完了"), "processing": MessageLookupByLibrary.simpleMessage("処理中"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("動画を処理中"), "publicLinkCreated": MessageLookupByLibrary.simpleMessage("公開リンクが作成されました"), @@ -1322,9 +1307,9 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("サポートを受ける"), "rateTheApp": MessageLookupByLibrary.simpleMessage("アプリを評価"), "rateUs": MessageLookupByLibrary.simpleMessage("評価して下さい"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("\"自分\" を再割り当て"), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("再割り当て中..."), "recover": MessageLookupByLibrary.simpleMessage("復元"), "recoverAccount": MessageLookupByLibrary.simpleMessage("アカウントを復元"), @@ -1332,7 +1317,7 @@ class MessageLookup extends MessageLookupByLibrary { "recoveryAccount": MessageLookupByLibrary.simpleMessage("アカウントを復元"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("リカバリが開始されました"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("リカバリーキー"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage("リカバリーキーはクリップボードにコピーされました"), @@ -1346,12 +1331,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("リカバリキーが確認されました"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "パスワードを忘れた場合、リカバリーキーは写真を復元するための唯一の方法になります。なお、設定 > アカウント でリカバリーキーを確認することができます。\n \n\nここにリカバリーキーを入力して、正しく保存できていることを確認してください。"), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("復元に成功しました!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "信頼する連絡先の持ち主があなたのアカウントにアクセスしようとしています"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "このデバイスではパスワードを確認する能力が足りません。\n\n恐れ入りますが、リカバリーキーを入力してパスワードを再生成する必要があります。"), "recreatePasswordTitle": @@ -1365,7 +1350,7 @@ class MessageLookup extends MessageLookupByLibrary { "referralStep1": MessageLookupByLibrary.simpleMessage("1. このコードを友達に贈りましょう"), "referralStep2": MessageLookupByLibrary.simpleMessage("2. 友達が有料プランに登録"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("リフェラル"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("リフェラルは現在一時停止しています"), @@ -1390,7 +1375,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeInvite": MessageLookupByLibrary.simpleMessage("招待を削除"), "removeLink": MessageLookupByLibrary.simpleMessage("リンクを削除"), "removeParticipant": MessageLookupByLibrary.simpleMessage("参加者を削除"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("人名を削除"), "removePublicLink": MessageLookupByLibrary.simpleMessage("公開リンクを削除"), "removePublicLinks": MessageLookupByLibrary.simpleMessage("公開リンクを削除"), @@ -1407,7 +1392,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("ファイル名を変更"), "renewSubscription": MessageLookupByLibrary.simpleMessage("サブスクリプションの更新"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("バグを報告"), "reportBug": MessageLookupByLibrary.simpleMessage("バグを報告"), "resendEmail": MessageLookupByLibrary.simpleMessage("メールを再送信"), @@ -1427,7 +1412,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("重複だと思うファイルを確認して削除してください"), "reviewSuggestions": MessageLookupByLibrary.simpleMessage("提案を確認"), "right": MessageLookupByLibrary.simpleMessage("右"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("回転"), "rotateLeft": MessageLookupByLibrary.simpleMessage("左に回転"), "rotateRight": MessageLookupByLibrary.simpleMessage("右に回転"), @@ -1474,8 +1459,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("友達を招待すると、共有される写真はここから閲覧できます"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage("処理と同期が完了すると、ここに人々が表示されます"), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("セキュリティ"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage("アプリ内で公開アルバムのリンクを見る"), @@ -1515,9 +1500,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "選択したアイテムはこの人としての登録が解除されますが、ライブラリからは削除されません。"), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, - "selfiesWithThem": m79, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("送信"), "sendEmail": MessageLookupByLibrary.simpleMessage("メールを送信する"), "sendInvite": MessageLookupByLibrary.simpleMessage("招待を送る"), @@ -1541,16 +1526,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("アルバムを開いて右上のシェアボタンをタップ"), "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("アルバムを共有"), "shareLink": MessageLookupByLibrary.simpleMessage("リンクの共有"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage("選んだ人と共有します"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Enteをダウンロードして、写真や動画の共有を簡単に!\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage("Enteを使っていない人に共有"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("アルバムの共有をしてみましょう"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1561,7 +1546,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("新しい共有写真"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage("誰かが写真を共有アルバムに追加した時に通知を受け取る"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("あなたと共有されたアルバム"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("あなたと共有されています"), "sharing": MessageLookupByLibrary.simpleMessage("共有中..."), @@ -1576,11 +1561,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("他のデバイスからサインアウトする"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "利用規約プライバシーポリシーに同意します"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage("全てのアルバムから削除されます。"), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("スキップ"), "social": MessageLookupByLibrary.simpleMessage("SNS"), "someItemsAreInBothEnteAndYourDevice": @@ -1611,8 +1596,8 @@ class MessageLookup extends MessageLookupByLibrary { "sortNewestFirst": MessageLookupByLibrary.simpleMessage("新しい順"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("古い順"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("成功✨"), - "sportsWithThem": m88, - "spotlightOnThem": m89, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("あなた自身にスポットライト!"), "startAccountRecoveryTitle": @@ -1624,14 +1609,14 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("ストレージ"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("ファミリー"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("あなた"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("ストレージの上限を超えました"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("動画の詳細"), "strongStrength": MessageLookupByLibrary.simpleMessage("強いパスワード"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("サブスクライブ"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "共有を有効にするには、有料サブスクリプションが必要です。"), @@ -1646,7 +1631,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("機能を提案"), "sunrise": MessageLookupByLibrary.simpleMessage("水平線"), "support": MessageLookupByLibrary.simpleMessage("サポート"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("同期が停止しました"), "syncing": MessageLookupByLibrary.simpleMessage("同期中..."), "systemTheme": MessageLookupByLibrary.simpleMessage("システム"), @@ -1654,7 +1639,7 @@ class MessageLookup extends MessageLookupByLibrary { "tapToEnterCode": MessageLookupByLibrary.simpleMessage("タップしてコードを入力"), "tapToUnlock": MessageLookupByLibrary.simpleMessage("タップして解除"), "tapToUpload": MessageLookupByLibrary.simpleMessage("タップしてアップロード"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "問題が発生したようです。しばらくしてから再試行してください。エラーが解決しない場合は、サポートチームにお問い合わせください。"), "terminate": MessageLookupByLibrary.simpleMessage("終了させる"), @@ -1673,7 +1658,7 @@ class MessageLookup extends MessageLookupByLibrary { "theme": MessageLookupByLibrary.simpleMessage("テーマ"), "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage("これらの項目はデバイスから削除されます。"), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage("全てのアルバムから削除されます。"), "thisActionCannotBeUndone": @@ -1690,12 +1675,12 @@ class MessageLookup extends MessageLookupByLibrary { "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage("この画像にEXIFデータはありません"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("これは私です"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("これはあなたの認証IDです"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("毎年のこの週"), - "thisWeekXYearsAgo": m98, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage("以下のデバイスからログアウトします:"), "thisWillLogYouOutOfThisDevice": @@ -1705,7 +1690,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "選択したすべてのクイックリンクの公開リンクを削除します。"), - "throughTheYears": m99, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "アプリのロックを有効にするには、システム設定でデバイスのパスコードまたは画面ロックを設定してください。"), @@ -1719,12 +1704,12 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("合計"), "totalSize": MessageLookupByLibrary.simpleMessage("合計サイズ"), "trash": MessageLookupByLibrary.simpleMessage("ゴミ箱"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("トリミング"), - "tripInYear": m101, - "tripToLocation": m102, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("信頼する連絡先"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("もう一度試してください"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "バックアップをオンにすると、このデバイスフォルダに追加されたファイルは自動的にEnteにアップロードされます。"), @@ -1739,7 +1724,7 @@ class MessageLookup extends MessageLookupByLibrary { "twofactorAuthenticationSuccessfullyReset": MessageLookupByLibrary.simpleMessage("2段階認証をリセットしました"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("2段階認証のセットアップ"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("アーカイブ解除"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("アルバムのアーカイブ解除"), "unarchiving": MessageLookupByLibrary.simpleMessage("アーカイブを解除中..."), @@ -1759,10 +1744,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("フォルダの選択を更新しています..."), "upgrade": MessageLookupByLibrary.simpleMessage("アップグレード"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage("アルバムにファイルをアップロード中"), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("1メモリを保存しています..."), "upto50OffUntil4thDec": @@ -1778,13 +1763,13 @@ class MessageLookup extends MessageLookupByLibrary { "useRecoveryKey": MessageLookupByLibrary.simpleMessage("リカバリーキーを使用"), "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("選択した写真を使用"), "usedSpace": MessageLookupByLibrary.simpleMessage("使用済み領域"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage("確認に失敗しました、再試行してください"), "verificationId": MessageLookupByLibrary.simpleMessage("確認用ID"), "verify": MessageLookupByLibrary.simpleMessage("確認"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Eメールの確認"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("確認"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("パスキーを確認"), "verifyPassword": MessageLookupByLibrary.simpleMessage("パスワードの確認"), @@ -1793,7 +1778,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("リカバリキーを確認中..."), "videoInfo": MessageLookupByLibrary.simpleMessage("ビデオ情報"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("ビデオ"), - "videoStreaming": MessageLookupByLibrary.simpleMessage("動画ストリーミング"), "videos": MessageLookupByLibrary.simpleMessage("ビデオ"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("アクティブなセッションを表示"), @@ -1818,7 +1802,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "あなたが所有していない写真やアルバムの編集はサポートされていません"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("弱いパスワード"), "welcomeBack": MessageLookupByLibrary.simpleMessage("おかえりなさい!"), "whatsNew": MessageLookupByLibrary.simpleMessage("最新情報"), @@ -1826,7 +1810,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("信頼する連絡先は、データの復旧が必要な際に役立ちます。"), "yearShort": MessageLookupByLibrary.simpleMessage("年"), "yearly": MessageLookupByLibrary.simpleMessage("年額"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("はい"), "yesCancel": MessageLookupByLibrary.simpleMessage("キャンセル"), "yesConvertToViewer": @@ -1839,7 +1823,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesRenew": MessageLookupByLibrary.simpleMessage("はい、更新する"), "yesResetPerson": MessageLookupByLibrary.simpleMessage("リセット"), "you": MessageLookupByLibrary.simpleMessage("あなた"), - "youAndThem": m112, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("ファミリープランに入会しています!"), "youAreOnTheLatestVersion": @@ -1856,7 +1840,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("自分自身と共有することはできません"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage("アーカイブした項目はありません"), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("アカウントは削除されました"), "yourMap": MessageLookupByLibrary.simpleMessage("あなたの地図"), diff --git a/mobile/lib/generated/intl/messages_lt.dart b/mobile/lib/generated/intl/messages_lt.dart index 4242687d1d..a2e44b6dd4 100644 --- a/mobile/lib/generated/intl/messages_lt.dart +++ b/mobile/lib/generated/intl/messages_lt.dart @@ -23,16 +23,20 @@ class MessageLookup extends MessageLookupByLibrary { static String m0(title) => "${title} (Aš)"; static String m1(count) => - "${Intl.plural(count, zero: 'Pridėti bendradarbių', one: 'Pridėti bendradarbį', few: 'Pridėti bendradarbius', many: 'Pridėti bendradarbio', other: 'Pridėti bendradarbių')}"; + "${Intl.plural(count, zero: 'Pridėti bendradarbių', one: 'Pridėti bendradarbį', other: 'Pridėti bendradarbių')}"; static String m2(count) => - "${Intl.plural(count, one: 'Pridėti elementą', few: 'Pridėti elementus', many: 'Pridėti elemento', other: 'Pridėti elementų')}"; + "${Intl.plural(count, one: 'Pridėti elementą', other: 'Pridėti elementų')}"; static String m3(storageAmount, endDate) => "Jūsų ${storageAmount} priedas galioja iki ${endDate}"; static String m4(count) => - "${Intl.plural(count, zero: 'Pridėti žiūrėtojų', one: 'Pridėti žiūrėtoją', few: 'Pridėti žiūrėtojus', many: 'Pridėti žiūrėtojo', other: 'Pridėti žiūrėtojų')}"; + "${Intl.plural(count, zero: 'Pridėti žiūrėtojų', one: 'Pridėti žiūrėtoją', other: 'Pridėti žiūrėtojų')}"; + + static String m5(emailOrName) => "Įtraukė ${emailOrName}"; + + static String m6(albumName) => "Sėkmingai įtraukta į „${albumName}“"; static String m7(name) => "Žavisi ${name}"; @@ -56,6 +60,9 @@ class MessageLookup extends MessageLookupByLibrary { 'other': 'Jūs gavote ${storageAmountInGb} GB iki šiol.', })}"; + static String m15(albumName) => + "Bendradarbiavimo nuoroda sukurta albumui „${albumName}“"; + static String m16(count) => "${Intl.plural(count, zero: 'Pridėta 0 bendradarbių', one: 'Pridėtas 1 bendradarbis', other: 'Pridėta ${count} bendradarbių')}"; @@ -71,7 +78,7 @@ class MessageLookup extends MessageLookupByLibrary { static String m20(endpoint) => "Prijungta prie ${endpoint}"; static String m21(count) => - "${Intl.plural(count, one: 'Ištrinti ${count} elementą', few: 'Ištrinti ${count} elementus', many: 'Ištrinti ${count} elemento', other: 'Ištrinti ${count} elementų')}"; + "${Intl.plural(count, one: 'Ištrinti ${count} elementą', other: 'Ištrinti ${count} elementų')}"; static String m22(currentlyDeleting, totalCount) => "Ištrinama ${currentlyDeleting} / ${totalCount}"; @@ -83,182 +90,188 @@ class MessageLookup extends MessageLookupByLibrary { "Iš savo registruoto el. pašto adreso atsiųskite el. laišką adresu ${supportEmail}"; static String m25(count, storageSaved) => - "Išvalėte ${Intl.plural(count, one: '${count} dubliuojantį failą', few: '${count} dubliuojančius failus', many: '${count} dubliuojančio failo', other: '${count} dubliuojančių failų')}, išsaugodami (${storageSaved})."; + "Išvalėte ${Intl.plural(count, one: '${count} dubliuojantį failą', other: '${count} dubliuojančių failų')}, išsaugodami (${storageSaved})"; static String m26(count, formattedSize) => "${count} failai (-ų), kiekvienas ${formattedSize}"; - static String m27(newEmail) => "El. paštas pakeistas į ${newEmail}"; + static String m28(newEmail) => "El. paštas pakeistas į ${newEmail}"; - static String m28(email) => "${email} neturi „Ente“ paskyros."; + static String m29(email) => "${email} neturi „Ente“ paskyros."; - static String m29(email) => + static String m30(email) => "${email} neturi „Ente“ paskyros.\n\nSiųskite jiems kvietimą bendrinti nuotraukas."; - static String m31(text) => "Rastos papildomos nuotraukos, skirtos ${text}"; + static String m32(text) => "Rastos papildomos nuotraukos, skirtos ${text}"; - static String m35(storageAmountInGB) => + static String m35(count, formattedNumber) => + "${Intl.plural(count, one: '${formattedNumber} failas šiame albume saugiai sukurta atsarginė kopija', few: '${formattedNumber} failai šiame albume saugiai sukurtos atsarginės kopijos', many: '${formattedNumber} failo šiame albume saugiai sukurtos atsargines kopijos', other: '${formattedNumber} failų šiame albume saugiai sukurta atsarginė kopija')}."; + + static String m36(storageAmountInGB) => "${storageAmountInGB} GB kiekvieną kartą, kai kas nors užsiregistruoja mokamam planui ir pritaiko jūsų kodą."; - static String m36(endDate) => + static String m37(endDate) => "Nemokamas bandomasis laikotarpis galioja iki ${endDate}"; - static String m37(count) => - "Vis dar galite pasiekti ${Intl.plural(count, one: 'jį', few: 'juos', many: 'juos', other: 'jų')} platformoje „Ente“, kol turite aktyvų prenumeratą."; + static String m38(count) => + "Vis dar galite pasiekti ${Intl.plural(count, one: 'jį', other: 'jų')} platformoje „Ente“, kol turite aktyvų prenumeratą."; - static String m38(sizeInMBorGB) => "Atlaisvinti ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Atlaisvinti ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m40(count, formattedSize) => + "${Intl.plural(count, one: 'Jį galima ištrinti iš įrenginio, kad atlaisvintų ${formattedSize}', other: 'Jų galima ištrinti iš įrenginio, kad atlaisvintų ${formattedSize}')}"; + + static String m41(currentlyProcessing, totalCount) => "Apdorojama ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => - "${Intl.plural(count, one: '${count} elementas', few: '${count} elementai', many: '${count} elemento', other: '${count} elementų')}"; + static String m43(count) => + "${Intl.plural(count, one: '${count} elementas', other: '${count} elementų')}"; - static String m44(email) => "${email} pakvietė jus būti patikimu kontaktu"; + static String m45(email) => "${email} pakvietė jus būti patikimu kontaktu"; - static String m45(expiryTime) => "Nuoroda nebegalios ${expiryTime}"; + static String m46(expiryTime) => "Nuoroda nebegalios ${expiryTime}"; - static String m47(personName, email) => + static String m48(personName, email) => "Tai susies ${personName} su ${email}."; - static String m48(count, formattedCount) => - "${Intl.plural(count, zero: 'nėra prisiminimų', one: '${formattedCount} prisiminimas', few: '${formattedCount} prisiminimai', many: '${formattedCount} prisiminimo', other: '${formattedCount} prisiminimų')}"; + static String m49(count, formattedCount) => + "${Intl.plural(count, zero: 'nėra prisiminimų', one: '${formattedCount} prisiminimas', other: '${formattedCount} prisiminimų')}"; - static String m49(count) => - "${Intl.plural(count, one: 'Perkelti elementą', few: 'Perkelti elementus', many: 'Perkelti elemento', other: 'Perkelti elementų')}"; + static String m50(count) => + "${Intl.plural(count, one: 'Perkelti elementą', other: 'Perkelti elementų')}"; - static String m51(personName) => "Nėra pasiūlymų asmeniui ${personName}."; + static String m51(albumName) => "Sėkmingai perkelta į „${albumName}“"; - static String m52(name) => "Ne ${name}?"; + static String m52(personName) => "Nėra pasiūlymų asmeniui ${personName}."; - static String m53(familyAdminEmail) => + static String m53(name) => "Ne ${name}?"; + + static String m54(familyAdminEmail) => "Susisiekite su ${familyAdminEmail}, kad pakeistumėte savo kodą."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Slaptažodžio stiprumas: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Kreipkitės į ${providerName} palaikymo komandą, jei jums buvo nuskaičiuota."; - static String m59(count) => - "${Intl.plural(count, zero: 'Nėra nuotraukų', one: '1 nuotrauka', few: '${count} nuotraukos', many: '${count} nuotraukos', other: '${count} nuotraukų')}"; - static String m60(count) => - "${Intl.plural(count, zero: '0 nuotraukų', one: '1 nuotrauka', few: '${count} nuotraukos', many: '${count} nuotraukos', other: '${count} nuotraukų')}"; + "${Intl.plural(count, zero: 'Nėra nuotraukų', one: '1 nuotrauka', other: '${count} nuotraukų')}"; - static String m61(endDate) => + static String m62(endDate) => "Nemokama bandomoji versija galioja iki ${endDate}.\nVėliau galėsite pasirinkti mokamą planą."; - static String m63(toEmail) => "Siųskite žurnalus adresu\n${toEmail}"; + static String m64(toEmail) => "Siųskite žurnalus adresu\n${toEmail}"; - static String m65(folderName) => "Apdorojama ${folderName}..."; + static String m66(folderName) => "Apdorojama ${folderName}..."; - static String m66(storeName) => "Vertinti mus parduotuvėje „${storeName}“"; + static String m67(storeName) => "Vertinti mus parduotuvėje „${storeName}“"; - static String m67(name) => "Perskirstė jus į ${name}"; + static String m68(name) => "Perskirstė jus į ${name}"; - static String m68(days, email) => + static String m69(days, email) => "Paskyrą galėsite pasiekti po ${days} dienų. Pranešimas bus išsiųstas į ${email}."; - static String m69(email) => + static String m70(email) => "Dabar galite atkurti ${email} paskyrą nustatydami naują slaptažodį."; - static String m70(email) => "${email} bando atkurti jūsų paskyrą."; + static String m71(email) => "${email} bando atkurti jūsų paskyrą."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Abu gaunate ${storageInGB} GB* nemokamai"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} bus pašalintas iš šio bendrinamo albumo.\n\nVisos jų pridėtos nuotraukos taip pat bus pašalintos iš albumo."; - static String m73(endDate) => "Prenumerata pratęsiama ${endDate}"; + static String m74(endDate) => "Prenumerata pratęsiama ${endDate}"; - static String m75(count) => - "${Intl.plural(count, one: 'Rastas ${count} rezultatas', few: 'Rasti ${count} rezultatai', many: 'Rasta ${count} rezultato', other: 'Rasta ${count} rezultatų')}"; + static String m76(count) => + "${Intl.plural(count, one: 'Rastas ${count} rezultatas', other: 'Rasta ${count} rezultatų')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Sekcijų ilgio neatitikimas: ${snapshotLength} != ${searchLength}"; - static String m77(count) => "${count} pasirinkta"; + static String m78(count) => "${count} pasirinkta"; - static String m78(count, yourCount) => + static String m79(count, yourCount) => "${count} pasirinkta (${yourCount} jūsų)"; - static String m80(verificationID) => + static String m81(verificationID) => "Štai mano patvirtinimo ID: ${verificationID}, skirta ente.io."; - static String m81(verificationID) => + static String m82(verificationID) => "Ei, ar galite patvirtinti, kad tai yra jūsų ente.io patvirtinimo ID: ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "„Ente“ rekomendacijos kodas: ${referralCode} \n\nTaikykite jį per Nustatymai → Bendrieji → Rekomendacijos, kad gautumėte ${referralStorageInGB} GB nemokamai po to, kai užsiregistruosite mokamam planui.\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Bendrinti su konkrečiais asmenimis', one: 'Bendrinta su 1 asmeniu', other: 'Bendrinta su ${numberOfPeople} asmenimis')}"; - static String m85(fileType) => - "Šis ${fileType} bus ištrintas iš jūsų įrenginio."; + static String m85(emailIDs) => "Bendrinta su ${emailIDs}"; static String m86(fileType) => + "Šis ${fileType} bus ištrintas iš jūsų įrenginio."; + + static String m87(fileType) => "Šis ${fileType} yra ir saugykloje „Ente“ bei įrenginyje."; - static String m87(fileType) => "Šis ${fileType} bus ištrintas iš „Ente“."; + static String m88(fileType) => "Šis ${fileType} bus ištrintas iš „Ente“."; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m92(id) => + static String m93(id) => "Jūsų ${id} jau susietas su kita „Ente“ paskyra.\nJei norite naudoti savo ${id} su šia paskyra, susisiekite su mūsų palaikymo komanda."; - static String m93(endDate) => "Jūsų prenumerata bus atsisakyta ${endDate}"; + static String m94(endDate) => "Jūsų prenumerata bus atsisakyta ${endDate}"; - static String m94(completed, total) => + static String m95(completed, total) => "${completed} / ${total} išsaugomi prisiminimai"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "Palieskite, kad įkeltumėte. Įkėlimas šiuo metu ignoruojamas dėl ${ignoreReason}."; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "Jie taip pat gauna ${storageAmountInGB} GB"; - static String m97(email) => "Tai – ${email} patvirtinimo ID"; + static String m98(email) => "Tai – ${email} patvirtinimo ID"; - static String m98(count) => - "${Intl.plural(count, one: 'Šią savaitę, prieš ${count} metus', few: 'Šią savaitę, prieš ${count} metus', many: 'Šią savaitę, prieš ${count} metų', other: 'Šią savaitę, prieš ${count} metų')}"; + static String m99(count) => + "${Intl.plural(count, one: 'Šią savaitę, prieš ${count} metus', other: 'Šią savaitę, prieš ${count} metų')}"; - static String m99(dateFormat) => "${dateFormat} per metus"; + static String m100(dateFormat) => "${dateFormat} per metus"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Netrukus', one: '1 diena', other: '${count} dienų')}"; - static String m101(year) => "Kelionė per ${year}"; + static String m102(year) => "Kelionė per ${year}"; - static String m102(location) => "Kelionė į ${location}"; + static String m103(location) => "Kelionė į ${location}"; - static String m103(email) => + static String m104(email) => "Buvote pakviesti tapti ${email} palikimo kontaktu."; - static String m104(galleryType) => + static String m105(galleryType) => "Galerijos tipas ${galleryType} nepalaikomas pervadinimui."; - static String m105(ignoreReason) => + static String m106(ignoreReason) => "Įkėlimas ignoruojamas dėl ${ignoreReason}."; - static String m107(endDate) => "Galioja iki ${endDate}"; + static String m107(count) => "Išsaugomi ${count} prisiminimai..."; - static String m108(email) => "Patvirtinti ${email}"; + static String m108(endDate) => "Galioja iki ${endDate}"; - static String m109(count) => - "${Intl.plural(count, zero: 'Pridėta 0 žiūrėtojų', one: 'Pridėtas 1 žiūrėtojas', few: 'Pridėti ${count} žiūrėtojai', many: 'Pridėta ${count} žiūrėtojo', other: 'Pridėta ${count} žiūrėtojų')}"; + static String m109(email) => "Patvirtinti ${email}"; - static String m110(email) => + static String m112(email) => "Išsiuntėme laišką adresu ${email}"; - static String m111(count) => - "${Intl.plural(count, one: 'prieš ${count} metus', few: 'prieš ${count} metus', many: 'prieš ${count} metų', other: 'prieš ${count} metų')}"; + static String m113(count) => + "${Intl.plural(count, one: 'prieš ${count} metus', other: 'prieš ${count} metų')}"; - static String m112(name) => "Jūs ir ${name}"; + static String m114(name) => "Jūs ir ${name}"; - static String m113(storageSaved) => "Sėkmingai atlaisvinote ${storageSaved}!"; + static String m115(storageSaved) => "Sėkmingai atlaisvinote ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -306,6 +319,8 @@ class MessageLookup extends MessageLookupByLibrary { "addViewer": MessageLookupByLibrary.simpleMessage("Pridėti žiūrėtoją"), "addViewers": m4, "addedAs": MessageLookupByLibrary.simpleMessage("Pridėta kaip"), + "addedBy": m5, + "addedSuccessfullyTo": m6, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Pridedama prie mėgstamų..."), "admiringThem": m7, @@ -398,6 +413,8 @@ class MessageLookup extends MessageLookupByLibrary { "Jūsų prenumerata buvo atšaukta. Ar norėtumėte pasidalyti priežastimi?"), "askDeleteReason": MessageLookupByLibrary.simpleMessage( "Kokia yra pagrindinė priežastis, dėl kurios ištrinate savo paskyrą?"), + "askYourLovedOnesToShare": MessageLookupByLibrary.simpleMessage( + "Paprašykite savo artimuosius bendrinti"), "atAFalloutShelter": MessageLookupByLibrary.simpleMessage("priešgaisrinėje slėptuvėje"), "authToChangeEmailVerificationSetting": @@ -424,6 +441,8 @@ class MessageLookup extends MessageLookupByLibrary { "Nustatykite tapatybę, kad peržiūrėtumėte savo aktyvius seansus"), "authToViewYourHiddenFiles": MessageLookupByLibrary.simpleMessage( "Nustatykite tapatybę, kad peržiūrėtumėte paslėptus failus"), + "authToViewYourMemories": MessageLookupByLibrary.simpleMessage( + "Nustatykite tapatybę, kad peržiūrėtumėte savo prisiminimus"), "authToViewYourRecoveryKey": MessageLookupByLibrary.simpleMessage( "Nustatykite tapatybę, kad peržiūrėtumėte savo atkūrimo raktą"), "authenticating": @@ -451,6 +470,8 @@ class MessageLookup extends MessageLookupByLibrary { "backgroundWithThem": m11, "backup": MessageLookupByLibrary.simpleMessage("Kurti atsarginę kopiją"), + "backupFailed": + MessageLookupByLibrary.simpleMessage("Atsarginė kopija nepavyko"), "backupFile": MessageLookupByLibrary.simpleMessage( "Kurti atsarginę failo kopiją"), "backupOverMobileData": MessageLookupByLibrary.simpleMessage( @@ -468,29 +489,19 @@ class MessageLookup extends MessageLookupByLibrary { "blackFridaySale": MessageLookupByLibrary.simpleMessage( "Juodojo penktadienio išpardavimas"), "blog": MessageLookupByLibrary.simpleMessage("Tinklaraštis"), - "cLBulkEdit": - MessageLookupByLibrary.simpleMessage("Masiškai redaguokite datas"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Dabar galite pasirinkti kelias nuotraukas ir vienu sparčiu veiksmu redaguoti visų nuotraukų datą ir laiką. Taip pat palaikomas datų perkėlimas."), - "cLFamilyPlan": - MessageLookupByLibrary.simpleMessage("Šeimos plano ribos"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Dabar galite nustatyti ribas, kiek saugyklos gali naudoti jūsų šeimos nariai."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Nauja piktograma"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Pagaliau – nauja programos piktograma, kuri, mūsų manymu, geriausiai atspindi mūsų kūrybą. Taip pat pridėjome piktogramos perjungiklį, tad galite ir toliau naudoti senąją piktogramą."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Prisiminimai"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Iš naujo atraskite ypatingas akimirkas – atkreipkite dėmesį į mėgstamus asmenis, keliones ir atostogas, geriausias nuotraukas bei daug daugiau. Įjunkite mašininį mokymąsi, pažymėkite save ir įvardykite draugus dėl geriausios patirties."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Valdikliai"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Dabar galima naudoti su prisiminimais integruotus pagrindinio ekrano valdiklius. Jie parodys jūsų ypatingas akimirkas neatvėrus programos."), "cachedData": MessageLookupByLibrary.simpleMessage("Podėliuoti duomenis"), + "calculating": MessageLookupByLibrary.simpleMessage("Skaičiuojama..."), "canNotOpenBody": MessageLookupByLibrary.simpleMessage( "Atsiprašome, šio albumo negalima atverti programoje."), "canNotOpenTitle": MessageLookupByLibrary.simpleMessage("Negalima atverti šio albumo"), + "canNotUploadToAlbumsOwnedByOthers": + MessageLookupByLibrary.simpleMessage( + "Negalima įkelti į kitiems priklausančius albumus"), + "canOnlyCreateLinkForFilesOwnedByYou": + MessageLookupByLibrary.simpleMessage( + "Galima sukurti nuorodą tik jums priklausantiems failams"), "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Galima pašalinti tik jums priklausančius failus"), "cancel": MessageLookupByLibrary.simpleMessage("Atšaukti"), @@ -548,6 +559,10 @@ class MessageLookup extends MessageLookupByLibrary { "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage( "• Spustelėkite ant perpildymo meniu"), "close": MessageLookupByLibrary.simpleMessage("Uždaryti"), + "clubByCaptureTime": MessageLookupByLibrary.simpleMessage( + "Grupuoti pagal užfiksavimo laiką"), + "clubByFileName": MessageLookupByLibrary.simpleMessage( + "Grupuoti pagal failo pavadinimą"), "clusteringProgress": MessageLookupByLibrary.simpleMessage("Sankaupos vykdymas"), "codeAppliedPageTitle": @@ -562,11 +577,15 @@ class MessageLookup extends MessageLookupByLibrary { "Sukurkite nuorodą, kad asmenys galėtų pridėti ir peržiūrėti nuotraukas bendrinamame albume, nereikalaujant „Ente“ programos ar paskyros. Puikiai tinka įvykių nuotraukoms rinkti."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Bendradarbiavimo nuoroda"), + "collaborativeLinkCreatedFor": m15, "collaborator": MessageLookupByLibrary.simpleMessage("Bendradarbis"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Bendradarbiai gali pridėti nuotraukų ir vaizdo įrašų į bendrintą albumą."), "collaboratorsSuccessfullyAdded": m16, + "collageLayout": MessageLookupByLibrary.simpleMessage("Išdėstymas"), + "collageSaved": MessageLookupByLibrary.simpleMessage( + "Koliažas išsaugotas į galeriją"), "collect": MessageLookupByLibrary.simpleMessage("Rinkti"), "collectEventPhotos": MessageLookupByLibrary.simpleMessage("Rinkti įvykių nuotraukas"), @@ -602,6 +621,8 @@ class MessageLookup extends MessageLookupByLibrary { "continueLabel": MessageLookupByLibrary.simpleMessage("Tęsti"), "continueOnFreeTrial": MessageLookupByLibrary.simpleMessage( "Tęsti nemokame bandomajame laikotarpyje"), + "convertToAlbum": + MessageLookupByLibrary.simpleMessage("Konvertuoti į albumą"), "copyEmailAddress": MessageLookupByLibrary.simpleMessage("Kopijuoti el. pašto adresą"), "copyLink": MessageLookupByLibrary.simpleMessage("Kopijuoti nuorodą"), @@ -614,6 +635,7 @@ class MessageLookup extends MessageLookupByLibrary { "Nepavyko atlaisvinti vietos."), "couldNotUpdateSubscription": MessageLookupByLibrary.simpleMessage( "Nepavyko atnaujinti prenumeratos"), + "count": MessageLookupByLibrary.simpleMessage("Skaičių"), "create": MessageLookupByLibrary.simpleMessage("Kurti"), "createAccount": MessageLookupByLibrary.simpleMessage("Kurti paskyrą"), "createAlbumActionHint": MessageLookupByLibrary.simpleMessage( @@ -646,6 +668,8 @@ class MessageLookup extends MessageLookupByLibrary { "declineTrustInvite": MessageLookupByLibrary.simpleMessage("Atmesti kvietimą"), "decrypting": MessageLookupByLibrary.simpleMessage("Iššifruojama..."), + "decryptingVideo": MessageLookupByLibrary.simpleMessage( + "Iššifruojamas vaizdo įrašas..."), "deduplicateFiles": MessageLookupByLibrary.simpleMessage("Atdubliuoti failus"), "delete": MessageLookupByLibrary.simpleMessage("Ištrinti"), @@ -705,6 +729,8 @@ class MessageLookup extends MessageLookupByLibrary { "developerSettingsWarning": MessageLookupByLibrary.simpleMessage( "Ar tikrai norite modifikuoti kūrėjo nustatymus?"), "deviceCodeHint": MessageLookupByLibrary.simpleMessage("Įveskite kodą"), + "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage( + "Į šį įrenginio albumą įtraukti failai bus automatiškai įkelti į „Ente“."), "deviceLock": MessageLookupByLibrary.simpleMessage("Įrenginio užraktas"), "deviceLockExplanation": MessageLookupByLibrary.simpleMessage( @@ -774,9 +800,9 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("El. paštas"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "El. paštas jau užregistruotas."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("El. paštas neregistruotas."), "emailVerificationToggle": @@ -807,6 +833,9 @@ class MessageLookup extends MessageLookupByLibrary { "Galutinis taškas sėkmingai atnaujintas"), "endtoendEncryptedByDefault": MessageLookupByLibrary.simpleMessage( "Pagal numatytąjį užšifruota visapusiškai"), + "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": + MessageLookupByLibrary.simpleMessage( + "„Ente“ gali užšifruoti ir išsaugoti failus tik tada, jei suteikiate prieigą prie jų."), "entePhotosPerm": MessageLookupByLibrary.simpleMessage( "„Ente“ reikia leidimo išsaugoti jūsų nuotraukas"), "enteSubscriptionPitch": MessageLookupByLibrary.simpleMessage( @@ -858,7 +887,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Eksportuoti duomenis"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Rastos papildomos nuotraukos"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Veidas dar nesugrupuotas. Grįžkite vėliau."), "faceRecognition": @@ -904,6 +933,7 @@ class MessageLookup extends MessageLookupByLibrary { "fileSavedToGallery": MessageLookupByLibrary.simpleMessage( "Failas išsaugotas į galeriją"), "fileTypes": MessageLookupByLibrary.simpleMessage("Failų tipai"), + "filesBackedUpInAlbum": m35, "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Failai išsaugoti į galeriją"), "findPeopleByName": MessageLookupByLibrary.simpleMessage( @@ -918,31 +948,34 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Rasti veidai"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Gauta nemokama saugykla"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Naudojama nemokama saugykla"), "freeTrial": MessageLookupByLibrary.simpleMessage( "Nemokamas bandomasis laikotarpis"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Atlaisvinti įrenginio vietą"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Sutaupykite vietos savo įrenginyje išvalydami failus, kurių atsarginės kopijos jau buvo sukurtos."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Atlaisvinti vietos"), + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Galerija"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Galerijoje rodoma iki 1000 prisiminimų"), "general": MessageLookupByLibrary.simpleMessage("Bendrieji"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generuojami šifravimo raktai..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Eiti į nustatymus"), "googlePlayId": MessageLookupByLibrary.simpleMessage("„Google Play“ ID"), + "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( + "Leiskite prieigą prie visų nuotraukų nustatymų programoje."), "grantPermission": MessageLookupByLibrary.simpleMessage("Suteikti leidimą"), "greenery": MessageLookupByLibrary.simpleMessage("Žaliasis gyvenimas"), @@ -974,6 +1007,8 @@ class MessageLookup extends MessageLookupByLibrary { "iOSOkButton": MessageLookupByLibrary.simpleMessage("Gerai"), "ignoreUpdate": MessageLookupByLibrary.simpleMessage("Ignoruoti"), "ignored": MessageLookupByLibrary.simpleMessage("ignoruota"), + "ignoredFolderUploadReason": MessageLookupByLibrary.simpleMessage( + "Kai kurie šio albumo failai ignoruojami, nes anksčiau buvo ištrinti iš „Ente“."), "imageNotAnalyzed": MessageLookupByLibrary.simpleMessage("Vaizdas neanalizuotas."), "immediately": MessageLookupByLibrary.simpleMessage("Iš karto"), @@ -1008,6 +1043,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Netinkamas raktas."), "invalidRecoveryKey": MessageLookupByLibrary.simpleMessage( "Į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ų."), + "invite": MessageLookupByLibrary.simpleMessage("Kviesti"), "inviteToEnte": MessageLookupByLibrary.simpleMessage("Kviesti į „Ente“"), "inviteYourFriends": @@ -1015,7 +1051,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Atrodo, kad kažkas nutiko ne taip. Bandykite pakartotinai po kurio laiko. Jei klaida tęsiasi, susisiekite su mūsų palaikymo komanda."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Elementai rodo likusių dienų skaičių iki visiško ištrynimo."), @@ -1051,7 +1087,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Palikimas"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Palikimo paskyros"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Palikimas leidžia patikimiems kontaktams pasiekti jūsų paskyrą jums nesant."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1059,12 +1095,14 @@ class MessageLookup extends MessageLookupByLibrary { "light": MessageLookupByLibrary.simpleMessage("Šviesi"), "lightTheme": MessageLookupByLibrary.simpleMessage("Šviesi"), "link": MessageLookupByLibrary.simpleMessage("Susieti"), + "linkCopiedToClipboard": MessageLookupByLibrary.simpleMessage( + "Nuoroda nukopijuota į iškarpinę"), "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Įrenginių riba"), "linkEmail": MessageLookupByLibrary.simpleMessage("Susieti el. paštą"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Įjungta"), "linkExpired": MessageLookupByLibrary.simpleMessage("Nebegalioja"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Nuorodos galiojimo laikas"), "linkHasExpired": @@ -1073,7 +1111,7 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Susiekite asmenį,"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "kad geriau bendrintumėte patirtį"), - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Gyvos nuotraukos"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Galite bendrinti savo prenumeratą su šeima."), @@ -1093,6 +1131,8 @@ class MessageLookup extends MessageLookupByLibrary { "web.ente.io turi sklandų įkėlėją"), "loadMessage9": MessageLookupByLibrary.simpleMessage( "Naudojame „Xchacha20Poly1305“, kad saugiai užšifruotume jūsų duomenis."), + "loadingExifData": + MessageLookupByLibrary.simpleMessage("Įkeliami EXIF duomenys..."), "loadingGallery": MessageLookupByLibrary.simpleMessage("Įkeliama galerija..."), "loadingModel": @@ -1129,6 +1169,9 @@ class MessageLookup extends MessageLookupByLibrary { "longPressAnEmailToVerifyEndToEndEncryption": MessageLookupByLibrary.simpleMessage( "Ilgai paspauskite el. paštą, kad patvirtintumėte visapusį šifravimą."), + "longpressOnAnItemToViewInFullscreen": + MessageLookupByLibrary.simpleMessage( + "Ilgai paspauskite elementą, kad peržiūrėtumėte per visą ekraną"), "loopVideoOff": MessageLookupByLibrary.simpleMessage( "Išjungtas vaizdo įrašo ciklas"), "loopVideoOn": MessageLookupByLibrary.simpleMessage( @@ -1157,7 +1200,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("„Mastodon“"), "matrix": MessageLookupByLibrary.simpleMessage("„Matrix“"), "me": MessageLookupByLibrary.simpleMessage("Aš"), - "memoryCount": m48, + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Atributika"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Sujungti su esamais"), @@ -1187,13 +1230,16 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Naujausią"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Aktualiausią"), "mountains": MessageLookupByLibrary.simpleMessage("Per kalvas"), - "moveItem": m49, + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Perkelti pasirinktas nuotraukas į vieną datą"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Perkelti į albumą"), + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Perkelta į šiukšlinę"), + "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( + "Perkeliami failai į albumą..."), "name": MessageLookupByLibrary.simpleMessage("Pavadinimą"), "nameTheAlbum": MessageLookupByLibrary.simpleMessage("Pavadinkite albumą"), @@ -1222,6 +1268,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nėra „Ente“ paskyros!"), "noExifData": MessageLookupByLibrary.simpleMessage("Nėra EXIF duomenų"), "noFacesFound": MessageLookupByLibrary.simpleMessage("Nerasta veidų."), + "noHiddenPhotosOrVideos": MessageLookupByLibrary.simpleMessage( + "Nėra paslėptų nuotraukų arba vaizdo įrašų"), "noImagesWithLocation": MessageLookupByLibrary.simpleMessage("Nėra vaizdų su vietove"), "noInternetConnection": @@ -1238,10 +1286,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Rezultatų nėra."), "noResultsFound": MessageLookupByLibrary.simpleMessage("Rezultatų nerasta."), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("Nerastas sistemos užraktas"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("Ne šis asmuo?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Kol kas su jumis niekuo nesibendrinama."), @@ -1253,7 +1301,8 @@ class MessageLookup extends MessageLookupByLibrary { "onEnte": MessageLookupByLibrary.simpleMessage( "Saugykloje ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("Vėl kelyje"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("Šią dieną"), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Tik jiems"), "oops": MessageLookupByLibrary.simpleMessage("Ups"), "oopsSomethingWentWrong": @@ -1263,6 +1312,8 @@ class MessageLookup extends MessageLookupByLibrary { "openAlbumInBrowserTitle": MessageLookupByLibrary.simpleMessage( "Naudokite interneto programą, kad pridėtumėte nuotraukų į šį albumą."), "openFile": MessageLookupByLibrary.simpleMessage("Atverti failą"), + "openSettings": + MessageLookupByLibrary.simpleMessage("Atverti nustatymus"), "openTheItem": MessageLookupByLibrary.simpleMessage("• Atverkite elementą."), "optionalAsShortAsYouLike": MessageLookupByLibrary.simpleMessage( @@ -1288,7 +1339,7 @@ class MessageLookup extends MessageLookupByLibrary { "Slaptažodis sėkmingai pakeistas"), "passwordLock": MessageLookupByLibrary.simpleMessage("Slaptažodžio užraktas"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "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ų."), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1299,7 +1350,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Mokėjimas nepavyko"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Deja, jūsų mokėjimas nepavyko. Susisiekite su palaikymo komanda ir mes jums padėsime!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Laukiami elementai"), "pendingSync": @@ -1315,12 +1366,11 @@ class MessageLookup extends MessageLookupByLibrary { "photoGridSize": MessageLookupByLibrary.simpleMessage("Nuotraukų tinklelio dydis"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("nuotrauka"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Nuotraukos"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Jūsų pridėtos nuotraukos bus pašalintos iš albumo"), - "photosCount": m60, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "Nuotraukos išlaiko santykinį laiko skirtumą"), @@ -1330,7 +1380,7 @@ class MessageLookup extends MessageLookupByLibrary { "Paleisti albumą televizoriuje"), "playOriginal": MessageLookupByLibrary.simpleMessage("Leisti originalą"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Leisti srautinį perdavimą"), "playstoreSubscription": @@ -1347,12 +1397,14 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Prisijunkite iš naujo."), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Pasirinkite sparčiąsias nuorodas, kad pašalintumėte"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Bandykite dar kartą."), "pleaseVerifyTheCodeYouHaveEntered": MessageLookupByLibrary.simpleMessage("Patvirtinkite įvestą kodą."), "pleaseWait": MessageLookupByLibrary.simpleMessage("Palaukite..."), + "pleaseWaitDeletingAlbum": MessageLookupByLibrary.simpleMessage( + "Palaukite. Ištrinamas albumas"), "pleaseWaitForSometimeBeforeRetrying": MessageLookupByLibrary.simpleMessage( "Palaukite kurį laiką prieš bandydami pakartotinai"), @@ -1377,17 +1429,19 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Tęsti"), "processed": MessageLookupByLibrary.simpleMessage("Apdorota"), "processing": MessageLookupByLibrary.simpleMessage("Apdorojama"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Apdorojami vaizdo įrašai"), + "publicLinkCreated": + MessageLookupByLibrary.simpleMessage("Vieša nuoroda sukurta"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage("Įjungta viešoji nuoroda"), "queued": MessageLookupByLibrary.simpleMessage("Įtraukta eilėje"), "raiseTicket": MessageLookupByLibrary.simpleMessage("Sukurti paraišką"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Vertinti programą"), "rateUs": MessageLookupByLibrary.simpleMessage("Vertinti mus"), - "rateUsOnStore": m66, - "reassignedToName": m67, + "rateUsOnStore": m67, + "reassignedToName": m68, "recover": MessageLookupByLibrary.simpleMessage("Atkurti"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Atkurti paskyrą"), @@ -1396,7 +1450,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Atkurti paskyrą"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Pradėtas atkūrimas"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Atkūrimo raktas"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Nukopijuotas atkūrimo raktas į iškarpinę"), @@ -1410,12 +1464,12 @@ class MessageLookup extends MessageLookupByLibrary { "Patvirtintas atkūrimo raktas"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Atkūrimo raktas – vienintelis būdas atkurti nuotraukas, jei pamiršote slaptažodį. Atkūrimo raktą galite rasti Nustatymose > Paskyra.\n\nĮveskite savo atkūrimo raktą čia, kad patvirtintumėte, ar teisingai jį išsaugojote."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Atkūrimas sėkmingas."), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Patikimas kontaktas bando pasiekti jūsų paskyrą."), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "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į)."), "recreatePasswordTitle": @@ -1431,7 +1485,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Duokite šį kodą savo draugams"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Jie užsiregistruoja mokamą planą"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Rekomendacijos"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Šiuo metu rekomendacijos yra pristabdytos"), @@ -1463,7 +1517,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Šalinti nuorodą"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Šalinti dalyvį"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Šalinti asmens žymą"), "removePublicLink": @@ -1479,16 +1533,20 @@ class MessageLookup extends MessageLookupByLibrary { "removingFromFavorites": MessageLookupByLibrary.simpleMessage("Pašalinama iš mėgstamų..."), "rename": MessageLookupByLibrary.simpleMessage("Pervadinti"), + "renameAlbum": + MessageLookupByLibrary.simpleMessage("Pervadinti albumą"), "renameFile": MessageLookupByLibrary.simpleMessage("Pervadinti failą"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Pratęsti prenumeratą"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Pranešti apie riktą"), "reportBug": MessageLookupByLibrary.simpleMessage("Pranešti apie riktą"), "resendEmail": MessageLookupByLibrary.simpleMessage("Iš naujo siųsti el. laišką"), + "resetIgnoredFiles": + MessageLookupByLibrary.simpleMessage("Atkurti ignoruojamus failus"), "resetPasswordTitle": MessageLookupByLibrary.simpleMessage( "Nustatyti slaptažodį iš naujo"), "resetToDefault": MessageLookupByLibrary.simpleMessage( @@ -1542,8 +1600,8 @@ class MessageLookup extends MessageLookupByLibrary { "Pakvieskite asmenis ir čia matysite visas jų bendrinamas nuotraukas."), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Asmenys bus rodomi čia, kai bus užbaigtas apdorojimas ir sinchronizavimas."), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Saugumas"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Žiūrėti viešų albumų nuorodas programoje"), @@ -1551,6 +1609,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Pasirinkite vietovę"), "selectALocationFirst": MessageLookupByLibrary.simpleMessage( "Pirmiausia pasirinkite vietovę"), + "selectAlbum": + MessageLookupByLibrary.simpleMessage("Pasirinkti albumą"), "selectAll": MessageLookupByLibrary.simpleMessage("Pasirinkti viską"), "selectAllShort": MessageLookupByLibrary.simpleMessage("Viskas"), "selectCoverPhoto": MessageLookupByLibrary.simpleMessage( @@ -1562,6 +1622,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Pasirinkite kalbą"), "selectMailApp": MessageLookupByLibrary.simpleMessage("Pasirinkti pašto programą"), + "selectMorePhotos": MessageLookupByLibrary.simpleMessage( + "Pasirinkti daugiau nuotraukų"), "selectOneDateAndTime": MessageLookupByLibrary.simpleMessage( "Pasirinkti vieną datą ir laiką"), "selectOneDateAndTimeForAll": MessageLookupByLibrary.simpleMessage( @@ -1581,8 +1643,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Pasirinkti elementai bus pašalinti iš šio asmens, bet nebus ištrinti iš jūsų bibliotekos."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "send": MessageLookupByLibrary.simpleMessage("Siųsti"), "sendEmail": MessageLookupByLibrary.simpleMessage("Siųsti el. laišką"), "sendInvite": MessageLookupByLibrary.simpleMessage("Siųsti kvietimą"), @@ -1613,24 +1675,28 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Bendrinti albumą dabar"), "shareLink": MessageLookupByLibrary.simpleMessage("Bendrinti nuorodą"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Bendrinkite tik su tais asmenimis, su kuriais norite"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Atsisiųskite „Ente“, kad galėtume lengvai bendrinti originalios kokybės nuotraukas ir vaizdo įrašus.\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Bendrinkite su ne „Ente“ naudotojais."), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, + "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( + "Bendrinkite savo pirmąjį albumą"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( "Sukurkite bendrinamus ir bendradarbiaujamus albumus su kitais „Ente“ naudotojais, įskaitant naudotojus nemokamuose planuose."), + "sharedByMe": MessageLookupByLibrary.simpleMessage("Bendrinta manimi"), "sharedByYou": MessageLookupByLibrary.simpleMessage("Bendrinta iš jūsų"), "sharedPhotoNotifications": MessageLookupByLibrary.simpleMessage( "Naujos bendrintos nuotraukos"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Gaukite pranešimus, kai kas nors prideda nuotrauką į bendrinamą albumą, kuriame dalyvaujate."), + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Bendrinta su manimi"), "sharedWithYou": @@ -1645,11 +1711,11 @@ class MessageLookup extends MessageLookupByLibrary { "Jei manote, kad kas nors gali žinoti jūsų slaptažodį, galite priverstinai atsijungti iš visų kitų įrenginių, naudojančių jūsų paskyrą."), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Sutinku su paslaugų sąlygomis ir privatumo politika"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Jis bus ištrintas iš visų albumų."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Praleisti"), "social": MessageLookupByLibrary.simpleMessage("Socialinės"), "someOfTheFilesYouAreTryingToDeleteAre": @@ -1692,14 +1758,14 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Stabdyti perdavimą"), "storage": MessageLookupByLibrary.simpleMessage("Saugykla"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Jūs"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Viršyta saugyklos riba."), "streamDetails": MessageLookupByLibrary.simpleMessage( "Srautinio perdavimo išsami informacija"), "strongStrength": MessageLookupByLibrary.simpleMessage("Stipri"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Prenumeruoti"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Kad įjungtumėte bendrinimą, reikia aktyvios mokamos prenumeratos."), @@ -1713,7 +1779,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Siūlyti funkcijas"), "sunrise": MessageLookupByLibrary.simpleMessage("Akiratyje"), "support": MessageLookupByLibrary.simpleMessage("Pagalba"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage( "Sinchronizavimas sustabdytas"), "syncing": MessageLookupByLibrary.simpleMessage("Sinchronizuojama..."), @@ -1726,7 +1792,7 @@ class MessageLookup extends MessageLookupByLibrary { "Palieskite, kad atrakintumėte"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Palieskite, kad įkeltumėte"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Atrodo, kad kažkas nutiko ne taip. Bandykite dar kartą po kurio laiko. Jei klaida tęsiasi, susisiekite su mūsų palaikymo komanda."), "terminate": MessageLookupByLibrary.simpleMessage("Baigti"), @@ -1746,7 +1812,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Įvestas atkūrimo raktas yra neteisingas."), "theme": MessageLookupByLibrary.simpleMessage("Tema"), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, + "thisAlbumAlreadyHDACollaborativeLink": + MessageLookupByLibrary.simpleMessage( + "Šis albumas jau turi bendradarbiavimo nuorodą."), "thisCanBeUsedToRecoverYourAccountIfYou": MessageLookupByLibrary.simpleMessage( "Tai gali būti naudojama paskyrai atkurti, jei prarandate dvigubo tapatybės nustatymą"), @@ -1756,12 +1825,12 @@ class MessageLookup extends MessageLookupByLibrary { "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Šis vaizdas neturi Exif duomenų"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Tai aš!"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("Tai – jūsų patvirtinimo ID"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("Ši savaitė per metus"), - "thisWeekXYearsAgo": m98, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Tai jus atjungs nuo toliau nurodyto įrenginio:"), @@ -1773,7 +1842,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Tai pašalins visų pasirinktų sparčiųjų nuorodų viešąsias nuorodas."), - "throughTheYears": m99, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "Kad įjungtumėte programos užraktą, sistemos nustatymuose nustatykite įrenginio prieigos kodą arba ekrano užraktą."), @@ -1786,15 +1855,18 @@ class MessageLookup extends MessageLookupByLibrary { "tooManyIncorrectAttempts": MessageLookupByLibrary.simpleMessage( "Per daug neteisingų bandymų."), "total": MessageLookupByLibrary.simpleMessage("iš viso"), + "totalSize": MessageLookupByLibrary.simpleMessage("Bendrą dydį"), "trash": MessageLookupByLibrary.simpleMessage("Šiukšlinė"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Trumpinti"), - "tripInYear": m101, - "tripToLocation": m102, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Patikimi kontaktai"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Bandyti dar kartą"), + "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( + "Įjunkite atsarginės kopijos kūrimą, kad automatiškai įkeltumėte į šį įrenginio aplanką įtrauktus failus į „Ente“."), "twitter": MessageLookupByLibrary.simpleMessage("„Twitter“"), "twoMonthsFreeOnYearlyPlans": MessageLookupByLibrary.simpleMessage( "2 mėnesiai nemokamai metiniuose planuose"), @@ -1808,7 +1880,7 @@ class MessageLookup extends MessageLookupByLibrary { "Dvigubas tapatybės nustatymas sėkmingai iš naujo nustatytas."), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Dvigubo tapatybės nustatymo sąranka"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Išarchyvuoti"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Išarchyvuoti albumą"), @@ -1819,6 +1891,10 @@ class MessageLookup extends MessageLookupByLibrary { "uncategorized": MessageLookupByLibrary.simpleMessage("Nekategorizuoti"), "unhide": MessageLookupByLibrary.simpleMessage("Rodyti"), + "unhideToAlbum": + MessageLookupByLibrary.simpleMessage("Rodyti į albumą"), + "unhidingFilesToAlbum": + MessageLookupByLibrary.simpleMessage("Rodomi failai į albumą"), "unlock": MessageLookupByLibrary.simpleMessage("Atrakinti"), "unpinAlbum": MessageLookupByLibrary.simpleMessage("Atsegti albumą"), "unselectAll": MessageLookupByLibrary.simpleMessage("Nesirinkti visų"), @@ -1828,7 +1904,12 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Atnaujinamas aplankų pasirinkimas..."), "upgrade": MessageLookupByLibrary.simpleMessage("Keisti planą"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, + "uploadingFilesToAlbum": + MessageLookupByLibrary.simpleMessage("Įkeliami failai į albumą..."), + "uploadingMultipleMemories": m107, + "uploadingSingleMemory": + MessageLookupByLibrary.simpleMessage("Išsaugomas prisiminimas..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( "Iki 50% nuolaida, gruodžio 4 d."), "usableReferralStorageInfo": MessageLookupByLibrary.simpleMessage( @@ -1842,7 +1923,7 @@ class MessageLookup extends MessageLookupByLibrary { "useRecoveryKey": MessageLookupByLibrary.simpleMessage("Naudoti atkūrimo raktą"), "usedSpace": MessageLookupByLibrary.simpleMessage("Naudojama vieta"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Patvirtinimas nepavyko. Bandykite dar kartą."), @@ -1851,7 +1932,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Patvirtinti"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Patvirtinti el. paštą"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Patvirtinti"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Patvirtinti slaptaraktį"), @@ -1863,8 +1944,8 @@ class MessageLookup extends MessageLookupByLibrary { "videoInfo": MessageLookupByLibrary.simpleMessage("Vaizdo įrašo informacija"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("vaizdo įrašas"), - "videoStreaming": MessageLookupByLibrary.simpleMessage( - "Vaizdo įrašų srautinis perdavimas"), + "videoStreaming": + MessageLookupByLibrary.simpleMessage("Srautiniai vaizdo įrašai"), "videos": MessageLookupByLibrary.simpleMessage("Vaizdo įrašai"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Peržiūrėti aktyvius seansus"), @@ -1880,7 +1961,6 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Peržiūrėti atkūrimo raktą"), "viewer": MessageLookupByLibrary.simpleMessage("Žiūrėtojas"), - "viewersSuccessfullyAdded": m109, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Aplankykite web.ente.io, kad tvarkytumėte savo prenumeratą"), "waitingForVerification": @@ -1893,7 +1973,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Nepalaikome nuotraukų ir albumų redagavimo, kurių dar neturite."), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Silpna"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Sveiki sugrįžę!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Kas naujo"), @@ -1901,7 +1981,7 @@ class MessageLookup extends MessageLookupByLibrary { "Patikimas kontaktas gali padėti atkurti jūsų duomenis."), "yearShort": MessageLookupByLibrary.simpleMessage("m."), "yearly": MessageLookupByLibrary.simpleMessage("Metinis"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Taip"), "yesCancel": MessageLookupByLibrary.simpleMessage("Taip, atsisakyti"), "yesConvertToViewer": @@ -1913,20 +1993,26 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage( "Taip, nustatyti asmenį iš naujo"), "you": MessageLookupByLibrary.simpleMessage("Jūs"), - "youAndThem": m112, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("Esate šeimos plane!"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage("Esate naujausioje versijoje"), "youCanAtMaxDoubleYourStorage": MessageLookupByLibrary.simpleMessage( "* Galite daugiausiai padvigubinti savo saugyklą."), + "youCanManageYourLinksInTheShareTab": + MessageLookupByLibrary.simpleMessage( + "Nuorodas galite valdyti bendrinimo kortelėje."), + "youCanTrySearchingForADifferentQuery": + MessageLookupByLibrary.simpleMessage( + "Galite pabandyti ieškoti pagal kitą užklausą."), "youCannotDowngradeToThisPlan": MessageLookupByLibrary.simpleMessage( "Negalite pakeisti į šį planą"), "youCannotShareWithYourself": MessageLookupByLibrary.simpleMessage( "Negalite bendrinti su savimi."), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Neturite jokių archyvuotų elementų."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Jūsų paskyra ištrinta"), "yourMap": MessageLookupByLibrary.simpleMessage("Jūsų žemėlapis"), @@ -1949,6 +2035,9 @@ class MessageLookup extends MessageLookupByLibrary { "Jūsų patvirtinimo kodas nebegaliojantis."), "youveNoDuplicateFilesThatCanBeCleared": MessageLookupByLibrary.simpleMessage( - "Neturite dubliuotų failų, kuriuos būtų galima išvalyti.") + "Neturite dubliuotų failų, kuriuos būtų galima išvalyti."), + "youveNoFilesInThisAlbumThatCanBeDeleted": + MessageLookupByLibrary.simpleMessage( + "Neturite šiame albume failų, kuriuos būtų galima ištrinti.") }; } diff --git a/mobile/lib/generated/intl/messages_nl.dart b/mobile/lib/generated/intl/messages_nl.dart index b1c44c706e..cfb2b6bf72 100644 --- a/mobile/lib/generated/intl/messages_nl.dart +++ b/mobile/lib/generated/intl/messages_nl.dart @@ -22,9 +22,18 @@ class MessageLookup extends MessageLookupByLibrary { static String m0(title) => "${title} (Ik)"; + static String m1(count) => + "${Intl.plural(count, zero: 'Samenwerker toevoegen', one: 'Samenwerker toevoegen', other: 'Samenwerkers toevoegen')}"; + + static String m2(count) => + "${Intl.plural(count, one: 'Bestand toevoegen', other: 'Bestanden toevoegen')}"; + static String m3(storageAmount, endDate) => "Jouw ${storageAmount} add-on is geldig tot ${endDate}"; + static String m4(count) => + "${Intl.plural(count, zero: 'Kijker toevoegen', one: 'Kijker toevoegen', other: 'Kijkers toevoegen')}"; + static String m5(emailOrName) => "Toegevoegd door ${emailOrName}"; static String m6(albumName) => "Succesvol toegevoegd aan ${albumName}"; @@ -75,6 +84,9 @@ class MessageLookup extends MessageLookupByLibrary { static String m21(count) => "${Intl.plural(count, one: 'Verwijder ${count} bestand', other: 'Verwijder ${count} bestanden')}"; + static String m116(count) => + "Verwijder de foto\'s (en video\'s) van deze ${count} albums ook uit alle andere albums waar deze deel van uitmaken?"; + static String m22(currentlyDeleting, totalCount) => "Verwijderen van ${currentlyDeleting} / ${totalCount}"; @@ -90,208 +102,232 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} bestanden, elk ${formattedSize}"; - static String m27(newEmail) => "E-mailadres gewijzigd naar ${newEmail}"; + static String m27(name) => "Dit e-mailadres is al aan ${name} gekoppeld."; - static String m28(email) => "${email} heeft geen Ente account."; + static String m28(newEmail) => "E-mailadres gewijzigd naar ${newEmail}"; - static String m29(email) => + static String m29(email) => "${email} heeft geen Ente account."; + + static String m30(email) => "${email} heeft geen Ente account.\n\nStuur ze een uitnodiging om foto\'s te delen."; - static String m30(name) => "${name} omarmen"; + static String m31(name) => "${name} omarmen"; - static String m31(text) => "Extra foto\'s gevonden voor ${text}"; + static String m32(text) => "Extra foto\'s gevonden voor ${text}"; - static String m32(name) => "Feestmaal met ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 bestand', other: '${formattedNumber} bestanden')} in dit album zijn veilig geback-upt"; + static String m33(name) => "Feestmaal met ${name}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 bestand', other: '${formattedNumber} bestanden')} in dit album zijn veilig geback-upt"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 bestand', other: '${formattedNumber} bestanden')} in dit album is veilig geback-upt"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB telkens als iemand zich aanmeldt voor een betaald abonnement en je code toepast"; - static String m36(endDate) => "Gratis proefversie geldig tot ${endDate}"; + static String m37(endDate) => "Gratis proefversie geldig tot ${endDate}"; - static String m38(sizeInMBorGB) => "Maak ${sizeInMBorGB} vrij"; + static String m38(count) => + "Je hebt nog steeds toegang tot ${Intl.plural(count, one: 'het', other: 'ze')} op Ente zolang je een actief abonnement hebt"; - static String m40(currentlyProcessing, totalCount) => + static String m39(sizeInMBorGB) => "Maak ${sizeInMBorGB} vrij"; + + static String m40(count, formattedSize) => + "${Intl.plural(count, one: 'Het kan verwijderd worden van het apparaat om ${formattedSize} vrij te maken', other: 'Ze kunnen verwijderd worden van het apparaat om ${formattedSize} vrij te maken')}"; + + static String m41(currentlyProcessing, totalCount) => "Verwerken van ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Wandelen met ${name}"; + static String m42(name) => "Wandelen met ${name}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} item', other: '${count} items')}"; - static String m43(name) => "Laatste keer met ${name}"; + static String m44(name) => "Laatste keer met ${name}"; - static String m44(email) => + static String m45(email) => "${email} heeft je uitgenodigd om een vertrouwd contact te zijn"; - static String m45(expiryTime) => "Link vervalt op ${expiryTime}"; + static String m46(expiryTime) => "Link vervalt op ${expiryTime}"; - static String m46(email) => "Link persoon aan ${email}"; + static String m47(email) => "Link persoon aan ${email}"; - static String m47(personName, email) => + static String m48(personName, email) => "Dit linkt ${personName} aan ${email}"; - static String m50(albumName) => "Succesvol verplaatst naar ${albumName}"; + static String m49(count, formattedCount) => + "${Intl.plural(count, zero: 'geen herinneringen', one: '${formattedCount} herinnering', other: '${formattedCount} herinneringen')}"; - static String m51(personName) => "Geen suggesties voor ${personName}"; + static String m50(count) => + "${Intl.plural(count, one: 'Bestand verplaatsen', other: 'Bestanden verplaatsen')}"; - static String m52(name) => "Niet ${name}?"; + static String m51(albumName) => "Succesvol verplaatst naar ${albumName}"; - static String m53(familyAdminEmail) => + static String m52(personName) => "Geen suggesties voor ${personName}"; + + static String m53(name) => "Niet ${name}?"; + + static String m54(familyAdminEmail) => "Neem contact op met ${familyAdminEmail} om uw code te wijzigen."; - static String m54(name) => "Feest met ${name}"; + static String m55(name) => "Feest met ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Wachtwoord sterkte: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Praat met ${providerName} klantenservice als u in rekening bent gebracht"; - static String m57(name, age) => "${name} is ${age}!"; + static String m58(name, age) => "${name} is ${age}!"; - static String m58(name, age) => "${name} wordt binnenkort ${age}"; + static String m59(name, age) => "${name} wordt binnenkort ${age}"; - static String m59(count) => + static String m60(count) => "${Intl.plural(count, zero: 'Geen foto\'s', one: '1 foto', other: '${count} foto\'s')}"; - static String m61(endDate) => + static String m61(count) => + "${Intl.plural(count, zero: '0 foto\'s', one: '1 foto', other: '${count} foto\'s')}"; + + static String m62(endDate) => "Gratis proefperiode geldig tot ${endDate}.\nU kunt naderhand een betaald abonnement kiezen."; - static String m62(toEmail) => "Stuur ons een e-mail op ${toEmail}"; + static String m63(toEmail) => "Stuur ons een e-mail op ${toEmail}"; - static String m63(toEmail) => + static String m64(toEmail) => "Verstuur de logboeken alstublieft naar ${toEmail}"; - static String m64(name) => "Poseren met ${name}"; + static String m65(name) => "Poseren met ${name}"; - static String m65(folderName) => "Verwerken van ${folderName}..."; + static String m66(folderName) => "Verwerken van ${folderName}..."; - static String m66(storeName) => "Beoordeel ons op ${storeName}"; + static String m67(storeName) => "Beoordeel ons op ${storeName}"; - static String m67(name) => "Toegewezen aan ${name}"; + static String m68(name) => "Toegewezen aan ${name}"; - static String m68(days, email) => + static String m69(days, email) => "U krijgt toegang tot het account na ${days} dagen. Een melding zal worden verzonden naar ${email}."; - static String m69(email) => + static String m70(email) => "U kunt nu het account van ${email} herstellen door een nieuw wachtwoord in te stellen."; - static String m70(email) => "${email} probeert je account te herstellen."; + static String m71(email) => "${email} probeert je account te herstellen."; - static String m71(storageInGB) => + static String m72(storageInGB) => "Jullie krijgen allebei ${storageInGB} GB* gratis"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} zal worden verwijderd uit dit gedeelde album\n\nAlle door hen toegevoegde foto\'s worden ook uit het album verwijderd"; - static String m73(endDate) => "Wordt verlengd op ${endDate}"; + static String m74(endDate) => "Wordt verlengd op ${endDate}"; - static String m74(name) => "Roadtrip met ${name}"; + static String m75(name) => "Roadtrip met ${name}"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} resultaat gevonden', other: '${count} resultaten gevonden')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Lengte van secties komt niet overeen: ${snapshotLength} != ${searchLength}"; - static String m77(count) => "${count} geselecteerd"; + static String m117(count) => "${count} geselecteerd"; - static String m78(count, yourCount) => + static String m78(count) => "${count} geselecteerd"; + + static String m79(count, yourCount) => "${count} geselecteerd (${yourCount} van jou)"; - static String m79(name) => "Selfies met ${name}"; - - static String m80(verificationID) => - "Hier is mijn verificatie-ID: ${verificationID} voor ente.io."; + static String m80(name) => "Selfies met ${name}"; static String m81(verificationID) => + "Hier is mijn verificatie-ID: ${verificationID} voor ente.io."; + + static String m82(verificationID) => "Hey, kunt u bevestigen dat dit uw ente.io verificatie-ID is: ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Ente verwijzingscode: ${referralCode} \n\nPas het toe bij Instellingen → Algemeen → Verwijzingen om ${referralStorageInGB} GB gratis te krijgen nadat je je hebt aangemeld voor een betaald abonnement\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Deel met specifieke mensen', one: 'Gedeeld met 1 persoon', other: 'Gedeeld met ${numberOfPeople} mensen')}"; - static String m84(emailIDs) => "Gedeeld met ${emailIDs}"; - - static String m85(fileType) => - "Deze ${fileType} zal worden verwijderd van jouw apparaat."; + static String m85(emailIDs) => "Gedeeld met ${emailIDs}"; static String m86(fileType) => - "Deze ${fileType} staat zowel in Ente als op jouw apparaat."; + "Deze ${fileType} zal worden verwijderd van jouw apparaat."; static String m87(fileType) => + "Deze ${fileType} staat zowel in Ente als op jouw apparaat."; + + static String m88(fileType) => "Deze ${fileType} zal worden verwijderd uit Ente."; - static String m88(name) => "Sporten met ${name}"; + static String m89(name) => "Sporten met ${name}"; - static String m89(name) => "Spotlicht op ${name}"; + static String m90(name) => "Spotlicht op ${name}"; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} van ${totalAmount} ${totalStorageUnit} gebruikt"; - static String m92(id) => + static String m93(id) => "Jouw ${id} is al aan een ander Ente account gekoppeld.\nAls je jouw ${id} wilt gebruiken met dit account, neem dan contact op met onze klantenservice"; - static String m93(endDate) => "Uw abonnement loopt af op ${endDate}"; + static String m94(endDate) => "Uw abonnement loopt af op ${endDate}"; - static String m94(completed, total) => + static String m95(completed, total) => "${completed}/${total} herinneringen bewaard"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "Tik om te uploaden, upload wordt momenteel genegeerd vanwege ${ignoreReason}"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "Zij krijgen ook ${storageAmountInGB} GB"; - static String m97(email) => "Dit is de verificatie-ID van ${email}"; + static String m98(email) => "Dit is de verificatie-ID van ${email}"; - static String m98(count) => + static String m99(count) => "${Intl.plural(count, one: 'Deze week, ${count} jaar geleden', other: 'Deze week, ${count} jaren geleden')}"; - static String m99(dateFormat) => "${dateFormat} door de jaren"; + static String m100(dateFormat) => "${dateFormat} door de jaren"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Binnenkort', one: '1 dag', other: '${count} dagen')}"; - static String m101(year) => "Reis in ${year}"; + static String m102(year) => "Reis in ${year}"; - static String m102(location) => "Reis naar ${location}"; + static String m103(location) => "Reis naar ${location}"; - static String m103(email) => + static String m104(email) => "Je bent uitgenodigd om een legacy contact van ${email} te zijn."; - static String m104(galleryType) => + static String m105(galleryType) => "Galerijtype ${galleryType} wordt niet ondersteund voor hernoemen"; - static String m105(ignoreReason) => + static String m106(ignoreReason) => "Upload wordt genegeerd omdat ${ignoreReason}"; - static String m106(count) => "${count} herinneringen veiligstellen..."; + static String m107(count) => "${count} herinneringen veiligstellen..."; - static String m107(endDate) => "Geldig tot ${endDate}"; + static String m108(endDate) => "Geldig tot ${endDate}"; - static String m108(email) => "Verifieer ${email}"; + static String m109(email) => "Verifieer ${email}"; - static String m110(email) => - "We hebben een e-mail gestuurd naar ${email}"; + static String m110(name) => "Bekijk ${name} om te ontkoppelen"; static String m111(count) => + "${Intl.plural(count, zero: '0 kijkers toegevoegd', one: '1 kijker toegevoegd', other: '${count} kijkers toegevoegd')}"; + + static String m112(email) => + "We hebben een e-mail gestuurd naar ${email}"; + + static String m113(count) => "${Intl.plural(count, one: '${count} jaar geleden', other: '${count} jaar geleden')}"; - static String m112(name) => "Jij en ${name}"; + static String m114(name) => "Jij en ${name}"; - static String m113(storageSaved) => + static String m115(storageSaved) => "Je hebt ${storageSaved} succesvol vrijgemaakt!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -309,6 +345,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Welkom terug!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( "Ik begrijp dat als ik mijn wachtwoord verlies, ik mijn gegevens kan verliezen omdat mijn gegevens end-to-end versleuteld zijn."), + "actionNotSupportedOnFavouritesAlbum": + MessageLookupByLibrary.simpleMessage( + "Actie niet ondersteund op Favorieten album"), "activeSessions": MessageLookupByLibrary.simpleMessage("Actieve sessies"), "add": MessageLookupByLibrary.simpleMessage("Toevoegen"), @@ -317,9 +356,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nieuw e-mailadres toevoegen"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Samenwerker toevoegen"), + "addCollaborators": m1, "addFiles": MessageLookupByLibrary.simpleMessage("Bestanden toevoegen"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Toevoegen vanaf apparaat"), + "addItem": m2, "addLocation": MessageLookupByLibrary.simpleMessage("Locatie toevoegen"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Toevoegen"), @@ -334,6 +375,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Details van add-ons"), "addOnValidTill": m3, "addOns": MessageLookupByLibrary.simpleMessage("Add-ons"), + "addParticipants": + MessageLookupByLibrary.simpleMessage("Voeg deelnemers toe"), "addPhotos": MessageLookupByLibrary.simpleMessage("Foto\'s toevoegen"), "addSelected": MessageLookupByLibrary.simpleMessage("Voeg geselecteerde toe"), @@ -345,6 +388,7 @@ class MessageLookup extends MessageLookupByLibrary { "addTrustedContact": MessageLookupByLibrary.simpleMessage("Vertrouwd contact toevoegen"), "addViewer": MessageLookupByLibrary.simpleMessage("Voeg kijker toe"), + "addViewers": m4, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Voeg nu je foto\'s toe"), "addedAs": MessageLookupByLibrary.simpleMessage("Toegevoegd als"), @@ -518,23 +562,6 @@ class MessageLookup extends MessageLookupByLibrary { "blackFridaySale": MessageLookupByLibrary.simpleMessage("Black Friday-aanbieding"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLBulkEdit": - MessageLookupByLibrary.simpleMessage("Bulk datums wijzigen"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Je kunt nu meerdere foto\'s selecteren en de datum/tijd van ze allemaal bewerken met één snelle actie. Verschuiven van datums wordt ook ondersteund."), - "cLFamilyPlan": - MessageLookupByLibrary.simpleMessage("Familieplan limieten"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Je kunt nu limieten instellen voor hoeveel opslag je familieleden kunnen gebruiken."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Nieuw icoon"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Tot slot, een nieuwe app icoon waarvan we denken dat het ons werk het beste weergeeft. We hebben ook een icon-switcher toegevoegd zodat je het oude icoon kunt blijven gebruiken."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Herinneringen"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Ontdek nog eens je speciale momenten - spotlicht op je favoriete mensen, op je reizen en vakanties, op de beste plaatjes, en nog veel meer. Zet Machine Learning aan, tag jezelf en benoem je vrienden voor de beste ervaring."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widgets"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Widgets die zijn geïntegreerd met herinneringen zijn nu beschikbaar. Ze tonen je speciale momenten zonder de app te openen."), "cachedData": MessageLookupByLibrary.simpleMessage("Cachegegevens"), "calculating": MessageLookupByLibrary.simpleMessage("Berekenen..."), "canNotOpenBody": MessageLookupByLibrary.simpleMessage( @@ -704,6 +731,8 @@ class MessageLookup extends MessageLookupByLibrary { "criticalUpdateAvailable": MessageLookupByLibrary.simpleMessage( "Belangrijke update beschikbaar"), "crop": MessageLookupByLibrary.simpleMessage("Bijsnijden"), + "curatedMemories": + MessageLookupByLibrary.simpleMessage("Samengestelde herinneringen"), "currentUsageIs": MessageLookupByLibrary.simpleMessage("Huidig gebruik is "), "currentlyRunning": @@ -750,6 +779,7 @@ class MessageLookup extends MessageLookupByLibrary { "deleteItemCount": m21, "deleteLocation": MessageLookupByLibrary.simpleMessage("Verwijder locatie"), + "deleteMultipleAlbumDialog": m116, "deletePhotos": MessageLookupByLibrary.simpleMessage("Foto\'s verwijderen"), "deleteProgress": m22, @@ -840,6 +870,7 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m25, "duplicateItemsGroup": m26, "edit": MessageLookupByLibrary.simpleMessage("Bewerken"), + "editEmailAlreadyLinked": m27, "editLocation": MessageLookupByLibrary.simpleMessage("Locatie bewerken"), "editLocationTagTitle": @@ -855,16 +886,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-mail"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("E-mail is al geregistreerd."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("E-mail niet geregistreerd."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("E-mailverificatie"), "emailYourLogs": MessageLookupByLibrary.simpleMessage("E-mail uw logboeken"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Noodcontacten"), "empty": MessageLookupByLibrary.simpleMessage("Leeg"), @@ -928,6 +959,8 @@ class MessageLookup extends MessageLookupByLibrary { "Voer een geldig e-mailadres in."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage("Voer je e-mailadres in"), + "enterYourNewEmailAddress": MessageLookupByLibrary.simpleMessage( + "Voer uw nieuwe e-mailadres in"), "enterYourPassword": MessageLookupByLibrary.simpleMessage("Voer je wachtwoord in"), "enterYourRecoveryKey": @@ -945,7 +978,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exporteer je gegevens"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Extra foto\'s gevonden"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Gezicht nog niet geclusterd, kom later terug"), "faceRecognition": @@ -985,7 +1018,7 @@ class MessageLookup extends MessageLookupByLibrary { "faqs": MessageLookupByLibrary.simpleMessage("Veelgestelde vragen"), "favorite": MessageLookupByLibrary.simpleMessage("Toevoegen aan favorieten"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Feedback"), "file": MessageLookupByLibrary.simpleMessage("Bestand"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -999,8 +1032,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Bestandstype"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Bestandstypen en namen"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Bestanden verwijderd"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( @@ -1018,24 +1051,26 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Gezichten gevonden"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Gratis opslag geclaimd"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Gratis opslag bruikbaar"), "freeTrial": MessageLookupByLibrary.simpleMessage("Gratis proefversie"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Apparaatruimte vrijmaken"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Bespaar ruimte op je apparaat door bestanden die al geback-upt zijn te wissen."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Ruimte vrijmaken"), + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Galerij"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Tot 1000 herinneringen getoond in de galerij"), "general": MessageLookupByLibrary.simpleMessage("Algemeen"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Encryptiesleutels genereren..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Ga naar instellingen"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), @@ -1064,7 +1099,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Verberg gedeelde bestanden uit de galerij"), "hiding": MessageLookupByLibrary.simpleMessage("Verbergen..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("Gehost bij OSM France"), "howItWorks": MessageLookupByLibrary.simpleMessage("Hoe het werkt"), @@ -1121,7 +1156,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Het lijkt erop dat er iets fout is gegaan. Probeer het later opnieuw. Als de fout zich blijft voordoen, neem dan contact op met ons supportteam."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Bestanden tonen het aantal resterende dagen voordat ze permanent worden verwijderd"), @@ -1142,7 +1177,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Help ons alsjeblieft met deze informatie"), "language": MessageLookupByLibrary.simpleMessage("Taal"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Laatst gewijzigd"), "lastYearsTrip": MessageLookupByLibrary.simpleMessage("Reis van vorig jaar"), @@ -1156,7 +1191,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Legacy"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Legacy accounts"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Legacy geeft vertrouwde contacten toegang tot je account bij afwezigheid."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1173,7 +1208,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("voor sneller delen"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Ingeschakeld"), "linkExpired": MessageLookupByLibrary.simpleMessage("Verlopen"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Vervaldatum"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Link is vervallen"), @@ -1181,11 +1216,13 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Link persoon"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "voor een betere ervaring met delen"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Live foto"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "U kunt uw abonnement met uw familie delen"), + "loadMessage2": MessageLookupByLibrary.simpleMessage( + "We hebben tot nu toe meer dan 200 miljoen herinneringen bewaard"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "We bewaren 3 kopieën van uw bestanden, één in een ondergrondse kernbunker"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1270,6 +1307,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Ik"), + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Merchandise"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Samenvoegen met bestaand"), @@ -1301,13 +1339,14 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Meest recent"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Meest relevant"), "mountains": MessageLookupByLibrary.simpleMessage("Over de heuvels"), + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Verplaats de geselecteerde foto\'s naar één datum"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Verplaats naar album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Verplaatsen naar verborgen album"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Naar prullenbak verplaatst"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1362,10 +1401,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Geen resultaten"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Geen resultaten gevonden"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Geen systeemvergrendeling gevonden"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("Niet dezelfde persoon?"), "nothingSharedWithYouYet": @@ -1378,7 +1417,8 @@ class MessageLookup extends MessageLookupByLibrary { "onEnte": MessageLookupByLibrary.simpleMessage( "Op ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("Onderweg"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("Op deze dag"), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Alleen hen"), "oops": MessageLookupByLibrary.simpleMessage("Oeps"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1408,7 +1448,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("Koppeling voltooid"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage( "Verificatie is nog in behandeling"), "passkey": MessageLookupByLibrary.simpleMessage("Passkey"), @@ -1418,7 +1458,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Wachtwoord succesvol aangepast"), "passwordLock": MessageLookupByLibrary.simpleMessage("Wachtwoord slot"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "De wachtwoordsterkte wordt berekend aan de hand van de lengte van het wachtwoord, de gebruikte tekens en of het wachtwoord al dan niet in de top 10.000 van meest gebruikte wachtwoorden staat"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1429,7 +1469,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Betaling mislukt"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Helaas is je betaling mislukt. Neem contact op met support zodat we je kunnen helpen!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Bestanden in behandeling"), "pendingSync": MessageLookupByLibrary.simpleMessage( @@ -1443,20 +1483,21 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Permanent verwijderen"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "Permanent verwijderen van apparaat?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Naam van persoon"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Harige kameraden"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Foto beschrijvingen"), "photoGridSize": MessageLookupByLibrary.simpleMessage("Foto raster grootte"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("foto"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Foto\'s"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Foto\'s toegevoegd door u zullen worden verwijderd uit het album"), + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "Foto\'s behouden relatief tijdsverschil"), @@ -1469,7 +1510,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Album afspelen op TV"), "playOriginal": MessageLookupByLibrary.simpleMessage("Origineel afspelen"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Stream afspelen"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore abonnement"), @@ -1482,14 +1523,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Neem contact op met klantenservice als het probleem aanhoudt"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Geef alstublieft toestemming"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Log opnieuw in"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Selecteer snelle links om te verwijderen"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Probeer het nog eens"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1504,7 +1545,7 @@ class MessageLookup extends MessageLookupByLibrary { "Gelieve even te wachten voordat u opnieuw probeert"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Een ogenblik geduld, dit zal even duren."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("Logboeken voorbereiden..."), "preserveMore": MessageLookupByLibrary.simpleMessage("Meer bewaren"), @@ -1522,7 +1563,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Verder"), "processed": MessageLookupByLibrary.simpleMessage("Verwerkt"), "processing": MessageLookupByLibrary.simpleMessage("Verwerken"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Video\'s verwerken"), "publicLinkCreated": @@ -1535,10 +1576,10 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Meld probleem"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Beoordeel de app"), "rateUs": MessageLookupByLibrary.simpleMessage("Beoordeel ons"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("\"Ik\" opnieuw toewijzen"), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Opnieuw toewijzen..."), "recover": MessageLookupByLibrary.simpleMessage("Herstellen"), @@ -1549,7 +1590,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Account herstellen"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Herstel gestart"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Herstelsleutel"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Herstelsleutel gekopieerd naar klembord"), @@ -1563,12 +1604,12 @@ class MessageLookup extends MessageLookupByLibrary { "Herstel sleutel geverifieerd"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Je herstelsleutel is de enige manier om je foto\'s te herstellen als je je wachtwoord bent vergeten. Je vindt je herstelsleutel in Instellingen > Account.\n\nVoer hier je herstelsleutel in om te controleren of je hem correct hebt opgeslagen."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Herstel succesvol!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Een vertrouwd contact probeert toegang te krijgen tot je account"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Het huidige apparaat is niet krachtig genoeg om je wachtwoord te verifiëren, dus moeten we de code een keer opnieuw genereren op een manier die met alle apparaten werkt.\n\nLog in met behulp van uw herstelcode en genereer opnieuw uw wachtwoord (je kunt dezelfde indien gewenst opnieuw gebruiken)."), "recreatePasswordTitle": MessageLookupByLibrary.simpleMessage( @@ -1584,7 +1625,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Geef deze code aan je vrienden"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Ze registreren voor een betaald plan"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Referenties"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Verwijzingen zijn momenteel gepauzeerd"), @@ -1616,7 +1657,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Verwijder link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Deelnemer verwijderen"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Verwijder persoonslabel"), "removePublicLink": @@ -1638,7 +1679,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bestandsnaam wijzigen"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Abonnement verlengen"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Een fout melden"), "reportBug": MessageLookupByLibrary.simpleMessage("Fout melden"), "resendEmail": @@ -1664,7 +1705,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Suggesties beoordelen"), "right": MessageLookupByLibrary.simpleMessage("Rechts"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Roteren"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Roteer links"), "rotateRight": MessageLookupByLibrary.simpleMessage("Rechtsom draaien"), @@ -1720,8 +1761,8 @@ class MessageLookup extends MessageLookupByLibrary { "Nodig mensen uit, en je ziet alle foto\'s die door hen worden gedeeld hier"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Personen worden hier getoond zodra verwerking en synchroniseren voltooid is"), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Beveiliging"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Bekijk publieke album links in de app"), @@ -1759,6 +1800,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Selecteer je gezicht"), "selectYourPlan": MessageLookupByLibrary.simpleMessage("Kies uw abonnement"), + "selectedAlbums": m117, "selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage( "Geselecteerde bestanden staan niet op Ente"), "selectedFoldersWillBeEncryptedAndBackedUp": @@ -1770,9 +1812,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Geselecteerde bestanden worden van deze persoon verwijderd, maar niet uit uw bibliotheek verwijderd."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, - "selfiesWithThem": m79, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Verzenden"), "sendEmail": MessageLookupByLibrary.simpleMessage("E-mail versturen"), "sendInvite": @@ -1804,16 +1846,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Deel nu een album"), "shareLink": MessageLookupByLibrary.simpleMessage("Link delen"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Deel alleen met de mensen die u wilt"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Download Ente zodat we gemakkelijk foto\'s en video\'s in originele kwaliteit kunnen delen\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Delen met niet-Ente gebruikers"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Deel jouw eerste album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1824,7 +1866,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nieuwe gedeelde foto\'s"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Ontvang meldingen wanneer iemand een foto toevoegt aan een gedeeld album waar je deel van uitmaakt"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Gedeeld met mij"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Gedeeld met jou"), @@ -1842,11 +1884,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Log uit op andere apparaten"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Ik ga akkoord met de gebruiksvoorwaarden en privacybeleid"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Het wordt uit alle albums verwijderd."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Overslaan"), "social": MessageLookupByLibrary.simpleMessage("Sociale media"), "someItemsAreInBothEnteAndYourDevice": MessageLookupByLibrary.simpleMessage( @@ -1863,6 +1905,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Er is iets fout gegaan, probeer het opnieuw"), "sorry": MessageLookupByLibrary.simpleMessage("Sorry"), + "sorryBackupFailedDesc": MessageLookupByLibrary.simpleMessage( + "Sorry, we kunnen dit bestand nu niet back-uppen, we zullen het later opnieuw proberen."), "sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage( "Sorry, kon niet aan favorieten worden toegevoegd!"), "sorryCouldNotRemoveFromFavorites": @@ -1880,8 +1924,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nieuwste eerst"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Oudste eerst"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Succes"), - "sportsWithThem": m88, - "spotlightOnThem": m89, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Spotlicht op jezelf"), "startAccountRecoveryTitle": @@ -1895,14 +1939,14 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Opslagruimte"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Familie"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Jij"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Opslaglimiet overschreden"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Stream details"), "strongStrength": MessageLookupByLibrary.simpleMessage("Sterk"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Abonneer"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Je hebt een actief betaald abonnement nodig om delen mogelijk te maken."), @@ -1920,7 +1964,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Features voorstellen"), "sunrise": MessageLookupByLibrary.simpleMessage("Aan de horizon"), "support": MessageLookupByLibrary.simpleMessage("Ondersteuning"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronisatie gestopt"), "syncing": MessageLookupByLibrary.simpleMessage("Synchroniseren..."), @@ -1932,7 +1976,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Tik om te ontgrendelen"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Tik om te uploaden"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Het lijkt erop dat er iets fout is gegaan. Probeer het later opnieuw. Als de fout zich blijft voordoen, neem dan contact op met ons supportteam."), "terminate": MessageLookupByLibrary.simpleMessage("Beëindigen"), @@ -1956,7 +2000,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Deze bestanden zullen worden verwijderd van uw apparaat."), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Ze zullen uit alle albums worden verwijderd."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1974,12 +2018,12 @@ class MessageLookup extends MessageLookupByLibrary { "Deze foto heeft geen exif gegevens"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Dit ben ik!"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("Dit is uw verificatie-ID"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("Deze week door de jaren"), - "thisWeekXYearsAgo": m98, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Dit zal je uitloggen van het volgende apparaat:"), @@ -1991,7 +2035,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Hiermee worden openbare links van alle geselecteerde snelle links verwijderd."), - "throughTheYears": m99, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "Om appvergrendeling in te schakelen, moet u een toegangscode of schermvergrendeling instellen in uw systeeminstellingen."), @@ -2006,13 +2050,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("totaal"), "totalSize": MessageLookupByLibrary.simpleMessage("Totale grootte"), "trash": MessageLookupByLibrary.simpleMessage("Prullenbak"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Knippen"), - "tripInYear": m101, - "tripToLocation": m102, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Vertrouwde contacten"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Probeer opnieuw"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Schakel back-up in om bestanden die toegevoegd zijn aan deze map op dit apparaat automatisch te uploaden."), @@ -2031,7 +2075,7 @@ class MessageLookup extends MessageLookupByLibrary { "Tweestapsverificatie succesvol gereset"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Tweestapsverificatie"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Uit archief halen"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Album uit archief halen"), @@ -2057,10 +2101,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("Map selectie bijwerken..."), "upgrade": MessageLookupByLibrary.simpleMessage("Upgraden"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Bestanden worden geüpload naar album..."), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage( "1 herinnering veiligstellen..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -2078,7 +2122,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Gebruik geselecteerde foto"), "usedSpace": MessageLookupByLibrary.simpleMessage("Gebruikte ruimte"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verificatie mislukt, probeer het opnieuw"), @@ -2086,7 +2130,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Verificatie ID"), "verify": MessageLookupByLibrary.simpleMessage("Verifiëren"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Bevestig e-mail"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verifiëren"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Bevestig passkey"), @@ -2098,7 +2142,7 @@ class MessageLookup extends MessageLookupByLibrary { "videoInfo": MessageLookupByLibrary.simpleMessage("Video-info"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("video"), "videoStreaming": - MessageLookupByLibrary.simpleMessage("Video streamen"), + MessageLookupByLibrary.simpleMessage("Video\'s met stream"), "videos": MessageLookupByLibrary.simpleMessage("Video\'s"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Actieve sessies bekijken"), @@ -2112,9 +2156,11 @@ class MessageLookup extends MessageLookupByLibrary { "viewLargeFilesDesc": MessageLookupByLibrary.simpleMessage( "Bekijk bestanden die de meeste opslagruimte verbruiken."), "viewLogs": MessageLookupByLibrary.simpleMessage("Logboeken bekijken"), + "viewPersonToUnlink": m110, "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Toon herstelsleutel"), "viewer": MessageLookupByLibrary.simpleMessage("Kijker"), + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Bezoek alstublieft web.ente.io om uw abonnement te beheren"), "waitingForVerification": @@ -2127,7 +2173,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "We ondersteunen het bewerken van foto\'s en albums waar je niet de eigenaar van bent nog niet"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Zwak"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Welkom terug!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Nieuw"), @@ -2135,7 +2181,7 @@ class MessageLookup extends MessageLookupByLibrary { "Vertrouwde contacten kunnen helpen bij het herstellen van je data."), "yearShort": MessageLookupByLibrary.simpleMessage("jr"), "yearly": MessageLookupByLibrary.simpleMessage("Jaarlijks"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Ja"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ja, opzeggen"), "yesConvertToViewer": @@ -2149,7 +2195,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage("Ja, reset persoon"), "you": MessageLookupByLibrary.simpleMessage("Jij"), - "youAndThem": m112, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage( "U bent onderdeel van een familie abonnement!"), "youAreOnTheLatestVersion": @@ -2168,7 +2214,7 @@ class MessageLookup extends MessageLookupByLibrary { "Je kunt niet met jezelf delen"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "U heeft geen gearchiveerde bestanden."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Je account is verwijderd"), "yourMap": MessageLookupByLibrary.simpleMessage("Jouw kaart"), diff --git a/mobile/lib/generated/intl/messages_no.dart b/mobile/lib/generated/intl/messages_no.dart index c3d24e64f1..bc59362742 100644 --- a/mobile/lib/generated/intl/messages_no.dart +++ b/mobile/lib/generated/intl/messages_no.dart @@ -93,211 +93,211 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} filer, ${formattedSize} hver"; - static String m27(newEmail) => "E-postadressen er endret til ${newEmail}"; + static String m28(newEmail) => "E-postadressen er endret til ${newEmail}"; - static String m28(email) => "${email} har ikke en Ente-konto."; + static String m29(email) => "${email} har ikke en Ente-konto."; - static String m29(email) => + static String m30(email) => "${email} har ikke en Ente-konto.\n\nsender dem en invitasjon til å dele bilder."; - static String m30(name) => "Omfavner ${name}"; + static String m31(name) => "Omfavner ${name}"; - static String m31(text) => "Ekstra bilder funnet for ${text}"; + static String m32(text) => "Ekstra bilder funnet for ${text}"; - static String m32(name) => "Festing med ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 fil', other: '${formattedNumber} filer')} på denne enheten har blitt sikkerhetskopiert"; + static String m33(name) => "Festing med ${name}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 fil', other: '${formattedNumber} filer')} på denne enheten har blitt sikkerhetskopiert"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 fil', other: '${formattedNumber} filer')} I dette albumet har blitt sikkerhetskopiert"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB hver gang noen melder seg på en betalt plan og bruker koden din"; - static String m36(endDate) => "Prøveperioden varer til ${endDate}"; + static String m37(endDate) => "Prøveperioden varer til ${endDate}"; - static String m38(sizeInMBorGB) => "Frigjør ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Frigjør ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Behandler ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Tur med ${name}"; + static String m42(name) => "Tur med ${name}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} element', other: '${count} elementer')}"; - static String m43(name) => "Siste gang med ${name}"; + static String m44(name) => "Siste gang med ${name}"; - static String m44(email) => + static String m45(email) => "${email} har invitert deg til å være en betrodd kontakt"; - static String m45(expiryTime) => "Lenken utløper på ${expiryTime}"; + static String m46(expiryTime) => "Lenken utløper på ${expiryTime}"; - static String m46(email) => "Knytt personen til ${email}"; + static String m47(email) => "Knytt personen til ${email}"; - static String m47(personName, email) => + static String m48(personName, email) => "Dette knytter ${personName} til ${email}"; - static String m48(count, formattedCount) => + static String m49(count, formattedCount) => "${Intl.plural(count, zero: 'ingen minner', one: '${formattedCount} minne', other: '${formattedCount} minner')}"; - static String m49(count) => + static String m50(count) => "${Intl.plural(count, one: 'Flytt elementet', other: 'Flytt elementene')}"; - static String m50(albumName) => "Flyttet til ${albumName}"; + static String m51(albumName) => "Flyttet til ${albumName}"; - static String m51(personName) => "Ingen forslag for ${personName}"; + static String m52(personName) => "Ingen forslag for ${personName}"; - static String m52(name) => "Ikke ${name}?"; + static String m53(name) => "Ikke ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Vennligst kontakt ${familyAdminEmail} for å endre koden din."; - static String m54(name) => "Fest med ${name}"; + static String m55(name) => "Fest med ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Passordstyrke: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Snakk med ${providerName} kundestøtte hvis du ble belastet"; - static String m57(name, age) => "${name} er ${age}!"; + static String m58(name, age) => "${name} er ${age}!"; - static String m58(name, age) => "${name} fyller ${age} snart"; + static String m59(name, age) => "${name} fyller ${age} snart"; - static String m59(count) => + static String m60(count) => "${Intl.plural(count, zero: 'Ingen bilder', one: '1 bilde', other: '${count} bilder')}"; - static String m61(endDate) => + static String m62(endDate) => "Prøveperioden varer til ${endDate}.\nDu kan velge en betalt plan etterpå."; - static String m62(toEmail) => "Vennligst send oss en e-post på ${toEmail}"; + static String m63(toEmail) => "Vennligst send oss en e-post på ${toEmail}"; - static String m63(toEmail) => "Vennligst send loggene til \n${toEmail}"; + static String m64(toEmail) => "Vennligst send loggene til \n${toEmail}"; - static String m64(name) => "Poseringer med ${name}"; + static String m65(name) => "Poseringer med ${name}"; - static String m65(folderName) => "Behandler ${folderName}..."; + static String m66(folderName) => "Behandler ${folderName}..."; - static String m66(storeName) => "Vurder oss på ${storeName}"; + static String m67(storeName) => "Vurder oss på ${storeName}"; - static String m67(name) => + static String m68(name) => "Tildeler deg til ${name}${name}${name}${name}${name}"; - static String m68(days, email) => + static String m69(days, email) => "Du kan få tilgang til kontoen etter ${days} dager. En varsling vil bli sendt til ${email}."; - static String m69(email) => + static String m70(email) => "Du kan nå gjenopprette ${email} sin konto ved å sette et nytt passord."; - static String m70(email) => "${email} prøver å gjenopprette kontoen din."; + static String m71(email) => "${email} prøver å gjenopprette kontoen din."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Begge dere får ${storageInGB} GB* gratis"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} vil bli fjernet fra dette delte albumet\n\nAlle bilder lagt til av dem vil også bli fjernet fra albumet"; - static String m73(endDate) => "Abonnement fornyes på ${endDate}"; + static String m74(endDate) => "Abonnement fornyes på ${endDate}"; - static String m74(name) => "Biltur med ${name}"; + static String m75(name) => "Biltur med ${name}"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} resultat funnet', other: '${count} resultater funnet')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Uoverensstemmelse i seksjonslengde: ${snapshotLength} != ${searchLength}"; - static String m77(count) => "${count} valgt"; + static String m78(count) => "${count} valgt"; - static String m78(count, yourCount) => "${count} valgt (${yourCount} dine)"; + static String m79(count, yourCount) => "${count} valgt (${yourCount} dine)"; - static String m79(name) => "Selfier med ${name}"; - - static String m80(verificationID) => - "Her er min verifiserings-ID: ${verificationID} for ente.io."; + static String m80(name) => "Selfier med ${name}"; static String m81(verificationID) => + "Her er min verifiserings-ID: ${verificationID} for ente.io."; + + static String m82(verificationID) => "Hei, kan du bekrefte at dette er din ente.io verifiserings-ID: ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Gi vervekode: ${referralCode} \n\nBruk den i Innstillinger → General → Verving for å få ${referralStorageInGB} GB gratis etter at du har registrert deg for en betalt plan\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Del med bestemte personer', one: 'Delt med 1 person', other: 'Delt med ${numberOfPeople} personer')}"; - static String m84(emailIDs) => "Delt med ${emailIDs}"; - - static String m85(fileType) => - "Denne ${fileType} vil bli slettet fra enheten din."; + static String m85(emailIDs) => "Delt med ${emailIDs}"; static String m86(fileType) => + "Denne ${fileType} vil bli slettet fra enheten din."; + + static String m87(fileType) => "Denne ${fileType} er både i Ente og på enheten din."; - static String m87(fileType) => "Denne ${fileType} vil bli slettet fra Ente."; + static String m88(fileType) => "Denne ${fileType} vil bli slettet fra Ente."; - static String m88(name) => "Sport med ${name}"; + static String m89(name) => "Sport med ${name}"; - static String m89(name) => "Fremhev ${name}"; + static String m90(name) => "Fremhev ${name}"; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} av ${totalAmount} ${totalStorageUnit} brukt"; - static String m92(id) => + static String m93(id) => "Din ${id} er allerede koblet til en annen Ente-konto.\nHvis du ønsker å bruke din ${id} med denne kontoen, vennligst kontakt vår brukerstøtte\'\'"; - static String m93(endDate) => + static String m94(endDate) => "Abonnementet ditt blir avsluttet den ${endDate}"; - static String m94(completed, total) => "${completed}/${total} minner bevart"; + static String m95(completed, total) => "${completed}/${total} minner bevart"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "Trykk for å laste opp, opplasting er ignorert nå på grunn av ${ignoreReason}"; - static String m96(storageAmountInGB) => "De får også ${storageAmountInGB} GB"; + static String m97(storageAmountInGB) => "De får også ${storageAmountInGB} GB"; - static String m97(email) => "Dette er ${email} sin verifiserings-ID"; + static String m98(email) => "Dette er ${email} sin verifiserings-ID"; - static String m98(count) => + static String m99(count) => "${Intl.plural(count, one: 'Denne uka, ${count} år siden', other: 'Denne uka, ${count} år siden')}"; - static String m99(dateFormat) => "${dateFormat} gjennom årene"; + static String m100(dateFormat) => "${dateFormat} gjennom årene"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Snart', one: '1 dag', other: '${count} dager')}"; - static String m101(year) => "Reise i ${year}"; + static String m102(year) => "Reise i ${year}"; - static String m102(location) => "Reise til ${location}"; + static String m103(location) => "Reise til ${location}"; - static String m103(email) => + static String m104(email) => "Du er invitert til å være en betrodd kontakt av ${email}."; - static String m104(galleryType) => + static String m105(galleryType) => "Galleritype ${galleryType} støttes ikke for nytt navn"; - static String m105(ignoreReason) => + static String m106(ignoreReason) => "Opplastingen ble ignorert på grunn av ${ignoreReason}"; - static String m106(count) => "Bevarer ${count} minner..."; + static String m107(count) => "Bevarer ${count} minner..."; - static String m107(endDate) => "Gyldig til ${endDate}"; + static String m108(endDate) => "Gyldig til ${endDate}"; - static String m108(email) => "Verifiser ${email}"; + static String m109(email) => "Verifiser ${email}"; - static String m110(email) => + static String m112(email) => "Vi har sendt en e-post til ${email}"; - static String m111(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} år siden', other: '${count} år siden')}"; - static String m112(name) => "Du og ${name}"; + static String m114(name) => "Du og ${name}"; - static String m113(storageSaved) => "Du har frigjort ${storageSaved}!"; + static String m115(storageSaved) => "Du har frigjort ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -521,23 +521,6 @@ class MessageLookup extends MessageLookupByLibrary { "blackFridaySale": MessageLookupByLibrary.simpleMessage("Black Friday salg"), "blog": MessageLookupByLibrary.simpleMessage("Blogg"), - "cLBulkEdit": - MessageLookupByLibrary.simpleMessage("Masseendring av datoer"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Du kan nå velge flere bilder, og redigere dato/klokkeslett for alle med en rask handling. Forskyving av datoer støttes også."), - "cLFamilyPlan": MessageLookupByLibrary.simpleMessage( - "Begrensninger for familieabonnement"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Du kan nå sette grenser for hvor mye lagringsplass familiemedlemmer kan bruke."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Nytt ikon"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Endelig er et nytt appikon, som vi tror best representerer arbeidet vårt. Vi har også lagt til en icon-switcher slik at du kan fortsette å bruke det gamle ikonet."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Minner"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Gjenoppdag dine spesielle øyeblikk - fremhev dine favorittpersoner, dine turer og ferier, de beste bildene dine, og mye mer. Skru på maskinlæring, merk deg selv og navngi vennene dine for best mulig opplevelse."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widgeter"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Hjemmeskjermwidgeter som er integrert med minner er nå tilgjengelige. De vil vise dine spesielle øyeblikk uten å åpne appen."), "cachedData": MessageLookupByLibrary.simpleMessage("Bufrede data"), "calculating": MessageLookupByLibrary.simpleMessage("Beregner..."), "canNotOpenBody": MessageLookupByLibrary.simpleMessage( @@ -853,16 +836,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-post"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "E-postadressen er allerede registrert."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage( "E-postadressen er ikke registrert."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("E-postbekreftelse"), "emailYourLogs": MessageLookupByLibrary.simpleMessage("Send loggene dine på e-post"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Nødkontakter"), "empty": MessageLookupByLibrary.simpleMessage("Tom"), @@ -938,7 +921,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Eksporter dine data"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Ekstra bilder funnet"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Ansikt ikke gruppert ennå, vennligst kom tilbake senere"), "faceRecognition": @@ -977,7 +960,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("Ofte stilte spørsmål"), "faqs": MessageLookupByLibrary.simpleMessage("Ofte stilte spørsmål"), "favorite": MessageLookupByLibrary.simpleMessage("Favoritt"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Tilbakemelding"), "file": MessageLookupByLibrary.simpleMessage("Fil"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -991,8 +974,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Filtyper"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Filtyper og navn"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Filene er slettet"), "filesSavedToGallery": @@ -1009,13 +992,13 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Fant ansikter"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Gratis lagringplass aktivert"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Gratis lagringsplass som kan brukes"), "freeTrial": MessageLookupByLibrary.simpleMessage("Gratis prøveversjon"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Frigjør plass på enheten"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -1028,7 +1011,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("Generelt"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Genererer krypteringsnøkler..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Gå til innstillinger"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), @@ -1057,7 +1040,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Skjul delte elementer fra hjemgalleriet"), "hiding": MessageLookupByLibrary.simpleMessage("Skjuler..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("Hostet på OSM France"), "howItWorks": @@ -1114,7 +1097,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Det ser ut til at noe gikk galt. Prøv på nytt etter en stund. Hvis feilen vedvarer, kan du kontakte kundestøtte."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Elementer viser gjenværende dager før de slettes for godt"), @@ -1135,7 +1118,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Vær vennlig og hjelp oss med denne informasjonen"), "language": MessageLookupByLibrary.simpleMessage("Språk"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Sist oppdatert"), "lastYearsTrip": MessageLookupByLibrary.simpleMessage("Fjorårets tur"), "leave": MessageLookupByLibrary.simpleMessage("Forlat"), @@ -1146,7 +1129,7 @@ class MessageLookup extends MessageLookupByLibrary { "left": MessageLookupByLibrary.simpleMessage("Venstre"), "legacy": MessageLookupByLibrary.simpleMessage("Arv"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Eldre kontoer"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Arv-funksjonen lar betrodde kontakter få tilgang til kontoen din i ditt fravær."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1162,7 +1145,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("for raskere deling"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktivert"), "linkExpired": MessageLookupByLibrary.simpleMessage("Utløpt"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Lenkeutløp"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Lenken har utløpt"), @@ -1170,8 +1153,8 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Knytt til person"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage("for bedre delingsopplevelse"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Live-bilder"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Du kan dele abonnementet med familien din"), @@ -1256,7 +1239,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Meg"), - "memoryCount": m48, + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Varer"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Slå sammen med eksisterende"), @@ -1288,13 +1271,13 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Nyeste"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Mest relevant"), "mountains": MessageLookupByLibrary.simpleMessage("Over åsene"), - "moveItem": m49, + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Flytt valgte bilder til en dato"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Flytt til album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Flytt til skjult album"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Flyttet til papirkurven"), "movingFilesToAlbum": @@ -1348,10 +1331,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Ingen resultater"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Ingen resultater funnet"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("Ingen systemlås funnet"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("Ikke denne personen?"), "nothingSharedWithYouYet": @@ -1364,7 +1347,7 @@ class MessageLookup extends MessageLookupByLibrary { "onEnte": MessageLookupByLibrary.simpleMessage( "På ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("På veien igjen"), - "onlyFamilyAdminCanChangeCode": m53, + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Bare de"), "oops": MessageLookupByLibrary.simpleMessage("Oisann"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1395,7 +1378,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("Sammenkobling fullført"), "panorama": MessageLookupByLibrary.simpleMessage("Panora"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage("Bekreftelse venter fortsatt"), "passkey": MessageLookupByLibrary.simpleMessage("Tilgangsnøkkel"), @@ -1405,7 +1388,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("Passordet ble endret"), "passwordLock": MessageLookupByLibrary.simpleMessage("Passordlås"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Passordstyrken beregnes basert på passordets lengde, brukte tegn, og om passordet finnes blant de 10 000 mest brukte passordene"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1416,7 +1399,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Betaling feilet"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Betalingen din mislyktes. Kontakt kundestøtte og vi vil hjelpe deg!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Ventende elementer"), "pendingSync": @@ -1430,16 +1413,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Slette for godt"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage("Slett permanent fra enhet?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Personnavn"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Pelsvenner"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Bildebeskrivelser"), "photoGridSize": MessageLookupByLibrary.simpleMessage("Bilderutenettstørrelse"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("bilde"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Bilder"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( @@ -1455,7 +1438,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Spill av album på TV"), "playOriginal": MessageLookupByLibrary.simpleMessage("Spill av original"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Spill av strøm"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore abonnement"), @@ -1468,14 +1451,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Vennligst kontakt kundestøtte hvis problemet vedvarer"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Vennligst gi tillatelser"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Vennligst logg inn igjen"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage("Velg hurtiglenker å fjerne"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Vennligst prøv igjen"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1489,7 +1472,7 @@ class MessageLookup extends MessageLookupByLibrary { "Vennligst vent en stund før du prøver på nytt"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Vennligst vent, dette vil ta litt tid."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("Forbereder logger..."), "preserveMore": MessageLookupByLibrary.simpleMessage("Behold mer"), @@ -1507,7 +1490,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Fortsett"), "processed": MessageLookupByLibrary.simpleMessage("Behandlet"), "processing": MessageLookupByLibrary.simpleMessage("Behandler"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Behandler videoer"), "publicLinkCreated": @@ -1520,9 +1503,9 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Opprett sak"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Vurder appen"), "rateUs": MessageLookupByLibrary.simpleMessage("Vurder oss"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("Tildel \"Meg\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Tildeler..."), "recover": MessageLookupByLibrary.simpleMessage("Gjenopprett"), @@ -1533,7 +1516,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Gjenopprett konto"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Gjenoppretting startet"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Gjenopprettingsnøkkel"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1548,12 +1531,12 @@ class MessageLookup extends MessageLookupByLibrary { "Gjenopprettingsnøkkel bekreftet"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Gjenopprettingsnøkkelen er den eneste måten å gjenopprette bildene dine på hvis du glemmer passordet ditt. Du finner gjenopprettingsnøkkelen din i Innstillinger > Konto.\n\nVennligst skriv inn gjenopprettingsnøkkelen din her for å bekrefte at du har lagret den riktig."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage( "Gjenopprettingen var vellykket!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "En betrodd kontakt prøver å få tilgang til kontoen din"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Den gjeldende enheten er ikke kraftig nok til å verifisere passordet ditt, men vi kan regenerere på en måte som fungerer på alle enheter.\n\nVennligst logg inn med gjenopprettingsnøkkelen og regenerer passordet (du kan bruke den samme igjen om du vil)."), "recreatePasswordTitle": @@ -1569,7 +1552,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Gi denne koden til vennene dine"), "referralStep2": MessageLookupByLibrary.simpleMessage( "De registrerer seg for en betalt plan"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Vervinger"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Vervinger er for øyeblikket satt på pause"), @@ -1600,7 +1583,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Fjern lenke"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Fjern deltaker"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Fjern etikett for person"), "removePublicLink": @@ -1621,7 +1604,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Gi nytt filnavn"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Forny abonnement"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Rapporter en feil"), "reportBug": MessageLookupByLibrary.simpleMessage("Rapporter feil"), "resendEmail": @@ -1647,7 +1630,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Gjennomgå forslag"), "right": MessageLookupByLibrary.simpleMessage("Høyre"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Roter"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Roter mot venstre"), "rotateRight": MessageLookupByLibrary.simpleMessage("Roter mot høyre"), @@ -1702,8 +1685,8 @@ class MessageLookup extends MessageLookupByLibrary { "Inviter folk, og du vil se alle bilder som deles av dem her"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Folk vil vises her når behandling og synkronisering er fullført"), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Sikkerhet"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Se offentlige albumlenker i appen"), @@ -1751,9 +1734,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Valgte elementer fjernes fra denne personen, men blir ikke slettet fra biblioteket ditt."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, - "selfiesWithThem": m79, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Send"), "sendEmail": MessageLookupByLibrary.simpleMessage("Send e-post"), "sendInvite": MessageLookupByLibrary.simpleMessage("Send invitasjon"), @@ -1783,16 +1766,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Del et album nå"), "shareLink": MessageLookupByLibrary.simpleMessage("Del link"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage("Del bare med de du vil"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Last ned Ente slik at vi lett kan dele bilder og videoer av original kvalitet\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Del med brukere som ikke har Ente"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Del ditt første album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1803,7 +1786,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nye delte bilder"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Motta varsler når noen legger til et bilde i et delt album som du er en del av"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Delt med meg"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Delt med deg"), "sharing": MessageLookupByLibrary.simpleMessage("Deler..."), @@ -1819,11 +1802,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Logg ut andre enheter"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Jeg godtar bruksvilkårene og personvernreglene"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Den vil bli slettet fra alle album."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Hopp over"), "social": MessageLookupByLibrary.simpleMessage("Sosial"), "someItemsAreInBothEnteAndYourDevice": @@ -1857,8 +1840,8 @@ class MessageLookup extends MessageLookupByLibrary { "sortNewestFirst": MessageLookupByLibrary.simpleMessage("Nyeste først"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Eldste først"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Suksess"), - "sportsWithThem": m88, - "spotlightOnThem": m89, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Fremhev deg selv"), "startAccountRecoveryTitle": @@ -1873,15 +1856,15 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Lagring"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Familie"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Deg"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Lagringsplassen er full"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Strømmedetaljer"), "strongStrength": MessageLookupByLibrary.simpleMessage("Sterkt"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Abonner"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Du trenger et aktivt betalt abonnement for å aktivere deling."), @@ -1899,7 +1882,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Foreslå funksjoner"), "sunrise": MessageLookupByLibrary.simpleMessage("På horisonten"), "support": MessageLookupByLibrary.simpleMessage("Brukerstøtte"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Synkronisering stoppet"), "syncing": MessageLookupByLibrary.simpleMessage("Synkroniserer..."), @@ -1912,7 +1895,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Trykk for å låse opp"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Trykk for å laste opp"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Det ser ut som noe gikk galt. Prøv på nytt etter en stund. Hvis feilen vedvarer, kontakt kundestøtte."), "terminate": MessageLookupByLibrary.simpleMessage("Avslutte"), @@ -1935,7 +1918,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Disse elementene vil bli slettet fra enheten din."), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "De vil bli slettet fra alle album."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1953,12 +1936,12 @@ class MessageLookup extends MessageLookupByLibrary { "Dette bildet har ingen exif-data"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Dette er meg!"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Dette er din bekreftelses-ID"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("Denne uka gjennom årene"), - "thisWeekXYearsAgo": m98, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Dette vil logge deg ut av følgende enhet:"), @@ -1970,7 +1953,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Dette fjerner de offentlige lenkene av alle valgte hurtiglenker."), - "throughTheYears": m99, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "For å aktivere applås, vennligst angi passord eller skjermlås i systeminnstillingene."), @@ -1984,13 +1967,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("totalt"), "totalSize": MessageLookupByLibrary.simpleMessage("Total størrelse"), "trash": MessageLookupByLibrary.simpleMessage("Papirkurv"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Beskjær"), - "tripInYear": m101, - "tripToLocation": m102, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Betrodde kontakter"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Prøv igjen"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Slå på sikkerhetskopi for å automatisk laste opp filer lagt til denne enhetsmappen i Ente."), @@ -2008,7 +1991,7 @@ class MessageLookup extends MessageLookupByLibrary { "Tofaktorautentisering ble tilbakestilt"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Oppsett av to-faktor"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Opphev arkivering"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Gjenopprett album"), @@ -2032,10 +2015,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("Oppdaterer mappevalg..."), "upgrade": MessageLookupByLibrary.simpleMessage("Oppgrader"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Laster opp filer til albumet..."), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Bevarer 1 minne..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -2055,7 +2038,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bruk valgt bilde"), "usedSpace": MessageLookupByLibrary.simpleMessage("Benyttet lagringsplass"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Bekreftelse mislyktes, vennligst prøv igjen"), @@ -2064,7 +2047,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Bekreft"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Bekreft e-postadresse"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Bekreft"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Bekreft tilgangsnøkkel"), @@ -2075,8 +2058,6 @@ class MessageLookup extends MessageLookupByLibrary { "Verifiserer gjenopprettingsnøkkel..."), "videoInfo": MessageLookupByLibrary.simpleMessage("Videoinformasjon"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("video"), - "videoStreaming": - MessageLookupByLibrary.simpleMessage("Videostrømming"), "videos": MessageLookupByLibrary.simpleMessage("Videoer"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Vis aktive økter"), @@ -2103,7 +2084,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Vi støtter ikke redigering av bilder og album som du ikke eier ennå"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Svakt"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Velkommen tilbake!"), @@ -2112,7 +2093,7 @@ class MessageLookup extends MessageLookupByLibrary { "Betrodd kontakt kan hjelpe til med å gjenopprette dine data."), "yearShort": MessageLookupByLibrary.simpleMessage("år"), "yearly": MessageLookupByLibrary.simpleMessage("Årlig"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Ja"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ja, avslutt"), "yesConvertToViewer": @@ -2126,7 +2107,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage("Ja, tilbakestill person"), "you": MessageLookupByLibrary.simpleMessage("Deg"), - "youAndThem": m112, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage( "Du har et familieabonnement!"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage( @@ -2145,7 +2126,7 @@ class MessageLookup extends MessageLookupByLibrary { "Du kan ikke dele med deg selv"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Du har ingen arkiverte elementer."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Brukeren din har blitt slettet"), "yourMap": MessageLookupByLibrary.simpleMessage("Ditt kart"), diff --git a/mobile/lib/generated/intl/messages_pl.dart b/mobile/lib/generated/intl/messages_pl.dart index 743df3f8ee..f7263871db 100644 --- a/mobile/lib/generated/intl/messages_pl.dart +++ b/mobile/lib/generated/intl/messages_pl.dart @@ -83,161 +83,161 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} plików, każdy po ${formattedSize}"; - static String m27(newEmail) => "Adres e-mail został zmieniony na ${newEmail}"; + static String m28(newEmail) => "Adres e-mail został zmieniony na ${newEmail}"; - static String m29(email) => + static String m30(email) => "${email} nie posiada konta Ente.\n\nWyślij im zaproszenie do udostępniania zdjęć."; - static String m31(text) => "Znaleziono dodatkowe zdjęcia dla ${text}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 plikowi', other: '${formattedNumber} plikom')} na tym urządzeniu została bezpiecznie utworzona kopia zapasowa"; + static String m32(text) => "Znaleziono dodatkowe zdjęcia dla ${text}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 plikowi', other: '${formattedNumber} plikom')} na tym urządzeniu została bezpiecznie utworzona kopia zapasowa"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 plikowi', other: '${formattedNumber} plikom')} w tym albumie została bezpiecznie utworzona kopia zapasowa"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB za każdym razem, gdy ktoś zarejestruje się w płatnym planie i użyje twojego kodu"; - static String m36(endDate) => "Okres próbny ważny do ${endDate}"; + static String m37(endDate) => "Okres próbny ważny do ${endDate}"; - static String m38(sizeInMBorGB) => "Zwolnij ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Zwolnij ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Przetwarzanie ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} element', few: '${count} elementy', many: '${count} elementów', other: '${count} elementu')}"; - static String m44(email) => + static String m45(email) => "${email} zaprosił Cię do zostania zaufanym kontaktem"; - static String m45(expiryTime) => "Link wygaśnie ${expiryTime}"; + static String m46(expiryTime) => "Link wygaśnie ${expiryTime}"; - static String m50(albumName) => "Pomyślnie przeniesiono do ${albumName}"; + static String m51(albumName) => "Pomyślnie przeniesiono do ${albumName}"; - static String m51(personName) => "Brak sugestii dla ${personName}"; + static String m52(personName) => "Brak sugestii dla ${personName}"; - static String m52(name) => "Nie ${name}?"; + static String m53(name) => "Nie ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Skontaktuj się z ${familyAdminEmail}, aby zmienić swój kod."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Siła hasła: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Porozmawiaj ze wsparciem ${providerName} jeśli zostałeś obciążony"; - static String m61(endDate) => + static String m62(endDate) => "Bezpłatny okres próbny ważny do ${endDate}.\nNastępnie możesz wybrać płatny plan."; - static String m62(toEmail) => + static String m63(toEmail) => "Prosimy o kontakt mailowy pod adresem ${toEmail}"; - static String m63(toEmail) => "Prosimy wysłać logi do ${toEmail}"; + static String m64(toEmail) => "Prosimy wysłać logi do ${toEmail}"; - static String m65(folderName) => "Przetwarzanie ${folderName}..."; + static String m66(folderName) => "Przetwarzanie ${folderName}..."; - static String m66(storeName) => "Oceń nas na ${storeName}"; + static String m67(storeName) => "Oceń nas na ${storeName}"; - static String m68(days, email) => + static String m69(days, email) => "Możesz uzyskać dostęp do konta po dniu ${days} dni. Powiadomienie zostanie wysłane na ${email}."; - static String m69(email) => + static String m70(email) => "Możesz teraz odzyskać konto ${email} poprzez ustawienie nowego hasła."; - static String m70(email) => "${email} próbuje odzyskać Twoje konto."; + static String m71(email) => "${email} próbuje odzyskać Twoje konto."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Oboje otrzymujecie ${storageInGB} GB* za darmo"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} zostanie usunięty z tego udostępnionego albumu\n\nWszelkie dodane przez nich zdjęcia zostaną usunięte z albumu"; - static String m73(endDate) => "Subskrypcja odnowi się ${endDate}"; + static String m74(endDate) => "Subskrypcja odnowi się ${endDate}"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, one: 'Znaleziono ${count} wynik', few: 'Znaleziono ${count} wyniki', other: 'Znaleziono ${count} wyników')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Niezgodność długości sekcji: ${snapshotLength} != ${searchLength}"; - static String m77(count) => "Wybrano ${count}"; + static String m78(count) => "Wybrano ${count}"; - static String m78(count, yourCount) => + static String m79(count, yourCount) => "Wybrano ${count} (twoich ${yourCount})"; - static String m80(verificationID) => + static String m81(verificationID) => "Oto mój identyfikator weryfikacyjny: ${verificationID} dla ente.io."; - static String m81(verificationID) => + static String m82(verificationID) => "Hej, czy możesz potwierdzić, że to jest Twój identyfikator weryfikacyjny ente.io: ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Kod polecający: ${referralCode} \n\nZastosuj go w: Ustawienia → Ogólne → Polecanie, aby otrzymać ${referralStorageInGB} GB za darmo po zarejestrowaniu się w płatnym planie\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Udostępnione określonym osobom', one: 'Udostępnione 1 osobie', other: 'Udostępnione ${numberOfPeople} osobom')}"; - static String m84(emailIDs) => "Udostępnione z ${emailIDs}"; - - static String m85(fileType) => - "Ten ${fileType} zostanie usunięty z Twojego urządzenia."; + static String m85(emailIDs) => "Udostępnione z ${emailIDs}"; static String m86(fileType) => + "Ten ${fileType} zostanie usunięty z Twojego urządzenia."; + + static String m87(fileType) => "Ten ${fileType} jest zarówno w Ente, jak i na twoim urządzeniu."; - static String m87(fileType) => "Ten ${fileType} zostanie usunięty z Ente."; + static String m88(fileType) => "Ten ${fileType} zostanie usunięty z Ente."; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "Użyto ${usedAmount} ${usedStorageUnit} z ${totalAmount} ${totalStorageUnit}"; - static String m92(id) => + static String m93(id) => "Twoje ${id} jest już połączony z innym kontem Ente.\nJeśli chcesz użyć swojego ${id} za pomocą tego konta, skontaktuj się z naszym wsparciem technicznym"; - static String m93(endDate) => + static String m94(endDate) => "Twoja subskrypcja zostanie anulowana dnia ${endDate}"; - static String m94(completed, total) => + static String m95(completed, total) => "Zachowano ${completed}/${total} wspomnień"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "Naciśnij, aby przesłać, przesyłanie jest obecnie ignorowane z powodu ${ignoreReason}"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "Oni również otrzymują ${storageAmountInGB} GB"; - static String m97(email) => "To jest identyfikator weryfikacyjny ${email}"; + static String m98(email) => "To jest identyfikator weryfikacyjny ${email}"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Wkrótce', one: '1 dzień', few: '${count} dni', other: '${count} dni')}"; - static String m103(email) => + static String m104(email) => "Zostałeś zaproszony do bycia dziedzicznym kontaktem przez ${email}."; - static String m104(galleryType) => + static String m105(galleryType) => "Typ galerii ${galleryType} nie jest obsługiwany dla zmiany nazwy"; - static String m105(ignoreReason) => + static String m106(ignoreReason) => "Przesyłanie jest ignorowane z powodu ${ignoreReason}"; - static String m106(count) => "Zachowywanie ${count} wspomnień..."; + static String m107(count) => "Zachowywanie ${count} wspomnień..."; - static String m107(endDate) => "Ważne do ${endDate}"; + static String m108(endDate) => "Ważne do ${endDate}"; - static String m108(email) => "Zweryfikuj ${email}"; + static String m109(email) => "Zweryfikuj ${email}"; - static String m110(email) => + static String m112(email) => "Wysłaliśmy wiadomość na adres ${email}"; - static String m111(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} rok temu', few: '${count} lata temu', many: '${count} lat temu', other: '${count} lata temu')}"; - static String m113(storageSaved) => "Pomyślnie zwolniłeś/aś ${storageSaved}!"; + static String m115(storageSaved) => "Pomyślnie zwolniłeś/aś ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -770,8 +770,8 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("Adres e-mail"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "Adres e-mail jest już zarejestrowany."), - "emailChangedTo": m27, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage( "Adres e-mail nie jest zarejestrowany."), "emailVerificationToggle": @@ -854,7 +854,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Eksportuj swoje dane"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Znaleziono dodatkowe zdjęcia"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Twarz jeszcze nie zgrupowana, prosimy wrócić później"), "faceRecognition": @@ -906,8 +906,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Rodzaje plików"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Typy plików i nazwy"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Pliki usunięto"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Pliki zapisane do galerii"), @@ -923,13 +923,13 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Znaleziono twarze"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Bezpłatna pamięć, którą odebrano"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Darmowa pamięć użyteczna"), "freeTrial": MessageLookupByLibrary.simpleMessage("Darmowy okres próbny"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Zwolnij miejsce na urządzeniu"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -941,7 +941,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("Ogólne"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generowanie kluczy szyfrujących..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Przejdź do ustawień"), "googlePlayId": @@ -1024,7 +1024,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Wygląda na to, że coś poszło nie tak. Spróbuj ponownie po pewnym czasie. Jeśli błąd będzie się powtarzał, skontaktuj się z naszym zespołem pomocy technicznej."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Elementy pokazują liczbę dni pozostałych przed trwałym usunięciem"), @@ -1054,7 +1054,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Dziedzictwo"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Odziedziczone konta"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Dziedzictwo pozwala zaufanym kontaktom na dostęp do Twojego konta w razie Twojej nieobecności."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1069,7 +1069,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Połącz adres e-mail"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktywny"), "linkExpired": MessageLookupByLibrary.simpleMessage("Wygasł"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Wygaśnięcie linku"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Link wygasł"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Nigdy"), @@ -1196,7 +1196,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Przenieś do albumu"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Przenieś do ukrytego albumu"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Przeniesiono do kosza"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1249,10 +1249,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Brak wyników"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Nie znaleziono wyników"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Nie znaleziono blokady systemowej"), - "notPersonLabel": m52, + "notPersonLabel": m53, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Nic Ci jeszcze nie udostępniono"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage( @@ -1262,7 +1262,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Na urządzeniu"), "onEnte": MessageLookupByLibrary.simpleMessage("W ente"), - "onlyFamilyAdminCanChangeCode": m53, + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Tylko te"), "oops": MessageLookupByLibrary.simpleMessage("Ups"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1299,7 +1299,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Hasło zostało pomyślnie zmienione"), "passwordLock": MessageLookupByLibrary.simpleMessage("Blokada hasłem"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Siła hasła jest obliczana, biorąc pod uwagę długość hasła, użyte znaki, i czy hasło pojawi się w 10 000 najczęściej używanych haseł"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1310,7 +1310,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Płatność się nie powiodła"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Niestety Twoja płatność nie powiodła się. Skontaktuj się z pomocą techniczną, a my Ci pomożemy!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Oczekujące elementy"), "pendingSync": @@ -1340,7 +1340,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("Blokada PIN"), "playOnTv": MessageLookupByLibrary.simpleMessage( "Odtwórz album na telewizorze"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Subskrypcja PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1352,14 +1352,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Skontaktuj się z pomocą techniczną, jeśli problem będzie się powtarzał"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Prosimy przyznać uprawnienia"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Zaloguj się ponownie"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Prosimy wybrać szybkie linki do usunięcia"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Spróbuj ponownie"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1387,7 +1387,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Udostępnianie prywatne"), "proceed": MessageLookupByLibrary.simpleMessage("Kontynuuj"), "processed": MessageLookupByLibrary.simpleMessage("Przetworzone"), - "processingImport": m65, + "processingImport": m66, "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Utworzono publiczny link"), "publicLinkEnabled": @@ -1397,7 +1397,7 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Zgłoś"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Oceń aplikację"), "rateUs": MessageLookupByLibrary.simpleMessage("Oceń nas"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "recover": MessageLookupByLibrary.simpleMessage("Odzyskaj"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Odzyskaj konto"), @@ -1406,7 +1406,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Odzyskaj konto"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Odzyskiwanie rozpoczęte"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Klucz odzyskiwania"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1421,12 +1421,12 @@ class MessageLookup extends MessageLookupByLibrary { "Klucz odzyskiwania zweryfikowany"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Twój klucz odzyskiwania jest jedynym sposobem na odzyskanie zdjęć, jeśli zapomnisz hasła. Klucz odzyskiwania można znaleźć w Ustawieniach > Konto.\n\nWprowadź tutaj swój klucz odzyskiwania, aby sprawdzić, czy został zapisany poprawnie."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Odzyskano pomyślnie!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Zaufany kontakt próbuje uzyskać dostęp do Twojego konta"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Obecne urządzenie nie jest wystarczająco wydajne, aby zweryfikować hasło, ale możemy je wygenerować w sposób działający na wszystkich urządzeniach.\n\nZaloguj się przy użyciu klucza odzyskiwania i wygeneruj nowe hasło (jeśli chcesz, możesz ponownie użyć tego samego)."), "recreatePasswordTitle": @@ -1442,7 +1442,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Przekaż ten kod swoim znajomym"), "referralStep2": MessageLookupByLibrary.simpleMessage("2. Wykupują płatny plan"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Polecenia"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Wysyłanie poleceń jest obecnie wstrzymane"), @@ -1472,7 +1472,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Usuń link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Usuń użytkownika"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Usuń etykietę osoby"), "removePublicLink": @@ -1493,7 +1493,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Zmień nazwę pliku"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Odnów subskrypcję"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Zgłoś błąd"), "reportBug": MessageLookupByLibrary.simpleMessage("Zgłoś błąd"), "resendEmail": @@ -1571,8 +1571,8 @@ class MessageLookup extends MessageLookupByLibrary { "Zaproś ludzi, a zobaczysz tutaj wszystkie udostępnione przez nich zdjęcia"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Osoby będą wyświetlane tutaj po zakończeniu przetwarzania i synchronizacji"), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Bezpieczeństwo"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Zobacz publiczne linki do albumów w aplikacji"), @@ -1605,8 +1605,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Wybrane elementy zostaną usunięte ze wszystkich albumów i przeniesione do kosza."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "send": MessageLookupByLibrary.simpleMessage("Wyślij"), "sendEmail": MessageLookupByLibrary.simpleMessage("Wyślij e-mail"), "sendInvite": @@ -1635,16 +1635,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Udostępnij teraz album"), "shareLink": MessageLookupByLibrary.simpleMessage("Udostępnij link"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Udostępnij tylko ludziom, którym chcesz"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Pobierz Ente, abyśmy mogli łatwo udostępniać zdjęcia i wideo w oryginalnej jakości\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Udostępnij użytkownikom bez konta Ente"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Udostępnij swój pierwszy album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1657,7 +1657,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nowe udostępnione zdjęcia"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Otrzymuj powiadomienia, gdy ktoś doda zdjęcie do udostępnionego albumu, którego jesteś częścią"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Udostępnione ze mną"), "sharedWithYou": @@ -1674,11 +1674,11 @@ class MessageLookup extends MessageLookupByLibrary { "Wyloguj z pozostałych urządzeń"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Akceptuję warunki korzystania z usługi i politykę prywatności"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "To zostanie usunięte ze wszystkich albumów."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Pomiń"), "social": MessageLookupByLibrary.simpleMessage("Społeczność"), "someItemsAreInBothEnteAndYourDevice": @@ -1726,13 +1726,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Pamięć"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Rodzina"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Ty"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Przekroczono limit pamięci"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("Silne"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Subskrybuj"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Potrzebujesz aktywnej płatnej subskrypcji, aby włączyć udostępnianie."), @@ -1749,7 +1749,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Zaproponuj funkcje"), "support": MessageLookupByLibrary.simpleMessage("Wsparcie techniczne"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronizacja zatrzymana"), "syncing": MessageLookupByLibrary.simpleMessage("Synchronizowanie..."), @@ -1762,7 +1762,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Naciśnij, aby odblokować"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Naciśnij, aby przesłać"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Wygląda na to, że coś poszło nie tak. Spróbuj ponownie po pewnym czasie. Jeśli błąd będzie się powtarzał, skontaktuj się z naszym zespołem pomocy technicznej."), "terminate": MessageLookupByLibrary.simpleMessage("Zakończ"), @@ -1786,7 +1786,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Te elementy zostaną usunięte z Twojego urządzenia."), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Zostaną one usunięte ze wszystkich albumów."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1802,7 +1802,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ten e-mail jest już używany"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Ten obraz nie posiada danych exif"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "To jest Twój Identyfikator Weryfikacji"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1826,11 +1826,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("ogółem"), "totalSize": MessageLookupByLibrary.simpleMessage("Całkowity rozmiar"), "trash": MessageLookupByLibrary.simpleMessage("Kosz"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Przytnij"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Zaufane kontakty"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Spróbuj ponownie"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Włącz kopię zapasową, aby automatycznie przesyłać pliki dodane do folderu urządzenia do Ente."), @@ -1850,7 +1850,7 @@ class MessageLookup extends MessageLookupByLibrary { "Pomyślnie zresetowano uwierzytelnianie dwustopniowe"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Uwierzytelnianie dwustopniowe"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Przywróć z archiwum"), "unarchiveAlbum": @@ -1875,10 +1875,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Aktualizowanie wyboru folderu..."), "upgrade": MessageLookupByLibrary.simpleMessage("Ulepsz"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Przesyłanie plików do albumu..."), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage( "Zachowywanie 1 wspomnienia..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1896,7 +1896,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Użyj zaznaczone zdjęcie"), "usedSpace": MessageLookupByLibrary.simpleMessage("Zajęta przestrzeń"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Weryfikacja nie powiodła się, spróbuj ponownie"), @@ -1905,7 +1905,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Zweryfikuj"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Zweryfikuj adres e-mail"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Zweryfikuj"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Zweryfikuj klucz dostępu"), @@ -1943,7 +1943,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Nie wspieramy edycji zdjęć i albumów, których jeszcze nie posiadasz"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Słabe"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Witaj ponownie!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Co nowego"), @@ -1951,7 +1951,7 @@ class MessageLookup extends MessageLookupByLibrary { "Zaufany kontakt może pomóc w odzyskaniu Twoich danych."), "yearShort": MessageLookupByLibrary.simpleMessage("r"), "yearly": MessageLookupByLibrary.simpleMessage("Rocznie"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Tak"), "yesCancel": MessageLookupByLibrary.simpleMessage("Tak, anuluj"), "yesConvertToViewer": @@ -1983,7 +1983,7 @@ class MessageLookup extends MessageLookupByLibrary { "Nie możesz udostępnić samemu sobie"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Nie masz żadnych zarchiwizowanych elementów."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Twoje konto zostało usunięte"), "yourMap": MessageLookupByLibrary.simpleMessage("Twoja mapa"), diff --git a/mobile/lib/generated/intl/messages_pt.dart b/mobile/lib/generated/intl/messages_pt.dart index 179e745e90..8583931540 100644 --- a/mobile/lib/generated/intl/messages_pt.dart +++ b/mobile/lib/generated/intl/messages_pt.dart @@ -22,21 +22,12 @@ class MessageLookup extends MessageLookupByLibrary { static String m0(title) => "${title} (Eu)"; - static String m1(count) => - "${Intl.plural(count, one: 'Adicionar colaborador', other: 'Adicionar colaboradores')}"; - - static String m2(count) => - "${Intl.plural(count, one: 'Adicionar item', other: 'Adicionar itens')}"; - static String m3(storageAmount, endDate) => - "Seu complemento ${storageAmount} é válido até ${endDate}"; - - static String m4(count) => - "${Intl.plural(count, one: 'Adicionar visualizador', other: 'Adicionar visualizadores')}"; + "Seu addon ${storageAmount} é válido até o momento ${endDate}"; static String m5(emailOrName) => "Adicionado por ${emailOrName}"; - static String m6(albumName) => "Adicionado com sucesso a ${albumName}"; + static String m6(albumName) => "Adicionado com sucesso a ${albumName}"; static String m7(name) => "Admirando ${name}"; @@ -46,15 +37,15 @@ class MessageLookup extends MessageLookupByLibrary { static String m9(versionValue) => "Versão: ${versionValue}"; static String m10(freeAmount, storageUnit) => - "${freeAmount} ${storageUnit} livre"; + "${freeAmount} ${storageUnit} grátis"; static String m11(name) => "Vistas bonitas com ${name}"; static String m12(paymentProvider) => - "Primeiramente cancele sua assinatura existente do ${paymentProvider}"; + "Por favor, cancele primeiro a sua subscrição existente de ${paymentProvider}"; static String m13(user) => - "${user} Não poderá adicionar mais fotos a este álbum\n\nEles ainda conseguirão remover fotos existentes adicionadas por eles"; + "${user} não será capaz de adicionar mais fotos a este álbum\n\nEles ainda serão capazes de remover fotos existentes adicionadas por eles"; static String m14(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { @@ -72,278 +63,261 @@ class MessageLookup extends MessageLookupByLibrary { "Você está prestes a adicionar ${email} como contato confiável. Eles poderão recuperar sua conta se você estiver ausente por ${numOfDays} dias."; static String m18(familyAdminEmail) => - "Entre em contato com ${familyAdminEmail} para gerenciar sua assinatura"; + "Contacte ${familyAdminEmail} para gerir a sua subscrição"; static String m19(provider) => - "Entre em contato conosco em support@ente.io para gerenciar sua assinatura ${provider}."; + "Contacte-nos em support@ente.io para gerir a sua subscrição ${provider}"; - static String m20(endpoint) => "Conectado à ${endpoint}"; + static String m20(endpoint) => "Conectado a ${endpoint}"; static String m21(count) => - "${Intl.plural(count, one: 'Excluir ${count} item', other: 'Excluir ${count} itens')}"; + "${Intl.plural(count, one: 'Apagar ${count} item', other: 'Apagar ${count} itens')}"; static String m22(currentlyDeleting, totalCount) => - "Excluindo ${currentlyDeleting} / ${totalCount}"; + "Apagar ${currentlyDeleting} / ${totalCount}"; static String m23(albumName) => - "Isso removerá o link público para acessar \"${albumName}\"."; + "Isto removerá o link público para acessar \"${albumName}\"."; static String m24(supportEmail) => - "Envie um e-mail para ${supportEmail} a partir do seu endereço de e-mail registrado"; + "Envie um e-mail para ${supportEmail} a partir do seu endereço de e-mail registado"; static String m25(count, storageSaved) => - "Você limpou ${Intl.plural(count, one: '${count} arquivo duplicado', other: '${count} arquivos duplicados')}, salvando (${storageSaved}!)"; + "Você limpou ${Intl.plural(count, one: '${count} arquivo duplicado', other: '${count} arquivos duplicados')}, guardando (${storageSaved}!)"; static String m26(count, formattedSize) => "${count} arquivos, ${formattedSize} cada"; - static String m27(newEmail) => "E-mail alterado para ${newEmail}"; + static String m28(newEmail) => "Email alterado para ${newEmail}"; - static String m28(email) => "${email} não possui uma conta Ente."; + static String m29(email) => "${email} não possui uma conta Ente."; - static String m29(email) => - "${email} não tem uma conta Ente.\n\nEnvie-os um convite para compartilhar fotos."; + static String m30(email) => + "${email} não possui uma conta Ente.\n\nEnvie um convite para compartilhar fotos."; - static String m30(name) => "Abraçando ${name}"; + static String m31(name) => "Abraçando ${name}"; - static String m31(text) => "Fotos adicionais encontradas para ${text}"; + static String m32(text) => "Fotos extras encontradas para ${text}"; - static String m32(name) => "Tendo banquete com ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} deste dispositivo foi copiado com segurança"; + static String m33(name) => "Tendo banquete com ${name}"; static String m34(count, formattedNumber) => - "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} deste álbum foi copiado com segurança"; + "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} neste dispositivo teve um backup seguro"; - static String m35(storageAmountInGB) => - "${storageAmountInGB} GB cada vez que alguém se inscrever a um plano pago e aplicar seu código"; + static String m35(count, formattedNumber) => + "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} neste álbum teve um backup seguro"; - static String m36(endDate) => "A avaliação grátis acaba em ${endDate}"; + static String m36(storageAmountInGB) => + "${storageAmountInGB} GB sempre que alguém se inscreve num plano pago e aplica o seu código"; - static String m37(count) => - "Você ainda pode acessá-${Intl.plural(count, one: 'lo', other: 'los')} no Ente, contanto que você tenha uma assinatura ativa"; + static String m37(endDate) => "Teste gratuito válido até ${endDate}"; - static String m38(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Libertar ${sizeInMBorGB}"; - static String m39(count, formattedSize) => - "${Intl.plural(count, one: 'Ele pode ser excluído do dispositivo para liberar ${formattedSize}', other: 'Eles podem ser excluídos do dispositivo para liberar ${formattedSize}')}"; - - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Processando ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Caminhando com ${name}"; + static String m42(name) => "Caminhando com ${name}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} item', other: '${count} itens')}"; - static String m43(name) => "Últimos momentos com ${name}"; + static String m44(name) => "Últimos momentos com ${name}"; - static String m44(email) => + static String m45(email) => "${email} convidou você para ser um contato confiável"; - static String m45(expiryTime) => "O link expirará em ${expiryTime}"; + static String m46(expiryTime) => "O link expirará em ${expiryTime}"; - static String m46(email) => "Vincular pessoa a ${email}"; + static String m47(email) => "Vincular pessoa a ${email}"; - static String m47(personName, email) => + static String m48(personName, email) => "Isso vinculará ${personName} a ${email}"; - static String m48(count, formattedCount) => - "${Intl.plural(count, zero: 'sem memórias', one: '${formattedCount} memória', other: '${formattedCount} memórias')}"; + static String m51(albumName) => "Movido com sucesso para ${albumName}"; - static String m49(count) => - "${Intl.plural(count, one: 'Mover item', other: 'Mover itens')}"; + static String m52(personName) => "Sem sugestões para ${personName}"; - static String m50(albumName) => "Movido com sucesso para ${albumName}"; + static String m53(name) => "Não é ${name}?"; - static String m51(personName) => "Sem sugestões para ${personName}"; - - static String m52(name) => "Não é ${name}?"; - - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Entre em contato com ${familyAdminEmail} para alterar o seu código."; - static String m54(name) => "Festejando com ${name}"; + static String m55(name) => "Festejando com ${name}"; - static String m55(passwordStrengthValue) => - "Força da senha: ${passwordStrengthValue}"; + static String m56(passwordStrengthValue) => + "Força da palavra-passe: ${passwordStrengthValue}"; - static String m56(providerName) => - "Fale com o suporte ${providerName} se você foi cobrado"; + static String m57(providerName) => + "Por favor, fale com o suporte ${providerName} se você foi cobrado"; - static String m57(name, age) => "${name} está com ${age}!"; + static String m58(name, age) => "${name} está com ${age}!"; - static String m58(name, age) => "${name} terá ${age} em breve"; - - static String m59(count) => - "${Intl.plural(count, zero: 'Sem fotos', one: '1 foto', other: '${count} fotos')}"; + static String m59(name, age) => "${name} terá ${age} em breve"; static String m60(count) => + "${Intl.plural(count, zero: 'Sem fotos', one: '1 foto', other: '${count} fotos')}"; + + static String m61(count) => "${Intl.plural(count, zero: '0 fotos', one: '1 foto', other: '${count} fotos')}"; - static String m61(endDate) => - "Avaliação grátis válida até ${endDate}.\nVocê pode alterar para um plano pago depois."; + static String m62(endDate) => + "Teste gratuito válido até ${endDate}.\nVocê pode escolher um plano pago depois."; - static String m62(toEmail) => "Envie-nos um e-mail para ${toEmail}"; + static String m63(toEmail) => + "Por favor, envie-nos um e-mail para ${toEmail}"; - static String m63(toEmail) => "Envie os registros para \n${toEmail}"; + static String m64(toEmail) => "Por favor, envie os logs para \n${toEmail}"; - static String m64(name) => "Fazendo pose com ${name}"; + static String m65(name) => "Fazendo pose com ${name}"; - static String m65(folderName) => "Processando ${folderName}..."; + static String m66(folderName) => "Processando ${folderName}..."; - static String m66(storeName) => "Avalie-nos no ${storeName}"; + static String m67(storeName) => "Avalie-nos em ${storeName}"; - static String m67(name) => "Atribuído a ${name}"; + static String m68(name) => "Atribuído a ${name}"; - static String m68(days, email) => + static String m69(days, email) => "Você poderá acessar a conta após ${days} dias. Uma notificação será enviada para ${email}."; - static String m69(email) => + static String m70(email) => "Você pode recuperar a conta com e-mail ${email} por definir uma nova senha."; - static String m70(email) => "${email} está tentando recuperar sua conta."; + static String m71(email) => "${email} está tentando recuperar sua conta."; - static String m71(storageInGB) => - "3. Ambos os dois ganham ${storageInGB} GB* grátis"; + static String m72(storageInGB) => "3. Ambos ganham ${storageInGB} GB* grátis"; - static String m72(userEmail) => - "${userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por eles também serão removidas do álbum"; + static String m73(userEmail) => + "${userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por elas também serão removidas do álbum"; - static String m73(endDate) => "Renovação de assinatura em ${endDate}"; + static String m74(endDate) => "A subscrição é renovada em ${endDate}"; - static String m74(name) => "Viajando de carro com ${name}"; + static String m75(name) => "Viajando de carro com ${name}"; - static String m75(count) => - "${Intl.plural(count, one: '${count} resultado encontrado', other: '${count} resultados encontrados')}"; + static String m76(count) => + "${Intl.plural(count, one: '${count} ano atrás', other: '${count} anos atrás')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Incompatibilidade de comprimento de seções: ${snapshotLength} != ${searchLength}"; - static String m77(count) => "${count} selecionado(s)"; + static String m78(count) => "${count} selecionado(s)"; - static String m78(count, yourCount) => + static String m79(count, yourCount) => "${count} selecionado(s) (${yourCount} seus)"; - static String m79(name) => "Tirando selfies com ${name}"; - - static String m80(verificationID) => - "Aqui está meu ID de verificação para o ente.io: ${verificationID}"; + static String m80(name) => "Tirando selfies com ${name}"; static String m81(verificationID) => - "Ei, você pode confirmar se este ID de verificação do ente.io é seu?: ${verificationID}"; + "Aqui está o meu ID de verificação: ${verificationID} para ente.io."; - static String m82(referralCode, referralStorageInGB) => - "Código de referência do Ente: ${referralCode} \n\nAplique-o em Configurações → Geral → Referências para obter ${referralStorageInGB} GB grátis após a sua inscrição num plano pago\n\nhttps://ente.io"; + static String m82(verificationID) => + "Ei, você pode confirmar que este é seu ID de verificação do ente.io: ${verificationID}"; - static String m83(numberOfPeople) => + static String m83(referralCode, referralStorageInGB) => + "Insira o código de referência: ${referralCode} \n\nAplique-o em Configurações → Geral → Indicações para obter ${referralStorageInGB} GB gratuitamente após a sua inscrição para um plano pago\n\nhttps://ente.io"; + + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Compartilhe com pessoas específicas', one: 'Compartilhado com 1 pessoa', other: 'Compartilhado com ${numberOfPeople} pessoas')}"; - static String m84(emailIDs) => "Compartilhado com ${emailIDs}"; - - static String m85(fileType) => - "Este ${fileType} será excluído do dispositivo."; + static String m85(emailIDs) => "Partilhado com ${emailIDs}"; static String m86(fileType) => - "Este ${fileType} está no Ente e em seu dispositivo."; + "Este ${fileType} será eliminado do seu dispositivo."; - static String m87(fileType) => "Este ${fileType} será excluído do Ente."; + static String m87(fileType) => + "Este ${fileType} encontra-se tanto no Ente como no seu dispositivo."; - static String m88(name) => "Jogando esportes com ${name}"; + static String m88(fileType) => "Este ${fileType} será eliminado do Ente."; - static String m89(name) => "Destacar ${name}"; + static String m89(name) => "Jogando esportes com ${name}"; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m90(name) => "Destacar ${name}"; - static String m91( + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; + + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} de ${totalAmount} ${totalStorageUnit} usado"; - static String m92(id) => - "Seu ${id} já está vinculado a outra conta Ente. Se você gostaria de usar seu ${id} com esta conta, entre em contato conosco\""; + static String m93(id) => + "Seu ${id} já está vinculado a outra conta Ente.\nSe você gostaria de usar seu ${id} com esta conta, por favor contate nosso suporte\'\'"; - static String m93(endDate) => "Sua assinatura será cancelada em ${endDate}"; + static String m94(endDate) => "A sua subscrição será cancelada em ${endDate}"; - static String m94(completed, total) => + static String m95(completed, total) => "${completed}/${total} memórias preservadas"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "Toque para enviar, atualmente o envio é ignorado devido a ${ignoreReason}"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "Eles também recebem ${storageAmountInGB} GB"; - static String m97(email) => "Este é o ID de verificação de ${email}"; + static String m98(email) => "Este é o ID de verificação de ${email}"; - static String m98(count) => + static String m99(count) => "${Intl.plural(count, one: 'Esta semana, ${count} ano atrás', other: 'Esta semana, ${count} anos atrás')}"; - static String m99(dateFormat) => "${dateFormat} com o passar dos anos"; + static String m100(dateFormat) => "${dateFormat} com o passar dos anos"; - static String m100(count) => - "${Intl.plural(count, zero: 'Em breve', one: '1 dia', other: '${count} dias')}"; + static String m101(count) => + "${Intl.plural(count, zero: 'Brevemente', one: '1 dia', other: '${count} dias')}"; - static String m101(year) => "Viajem em ${year}"; + static String m102(year) => "Viajem em ${year}"; - static String m102(location) => "Viajem à ${location}"; + static String m103(location) => "Viajem à ${location}"; - static String m103(email) => + static String m104(email) => "Você foi convidado para ser um contato legado por ${email}."; - static String m104(galleryType) => - "O tipo de galeria ${galleryType} não é suportado para renomear"; + static String m105(galleryType) => + "Tipo de galeria ${galleryType} não é permitido para renomear"; - static String m105(ignoreReason) => - "O envio é ignorado devido a ${ignoreReason}"; + static String m106(ignoreReason) => "Envio ignorado devido à ${ignoreReason}"; - static String m106(count) => "Preservando ${count} memórias..."; + static String m107(count) => "Preservar ${count} memórias..."; - static String m107(endDate) => "Válido até ${endDate}"; + static String m108(endDate) => "Válido até ${endDate}"; - static String m108(email) => "Verificar ${email}"; + static String m109(email) => "Verificar e-mail"; - static String m109(count) => - "${Intl.plural(count, zero: 'Adicionado 0 visualizadores', one: 'Adicionado 1 visualizador', other: 'Adicionado ${count} visualizadores')}"; + static String m112(email) => + "Enviamos um e-mail para ${email}"; - static String m110(email) => "Enviamos um e-mail à ${email}"; - - static String m111(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} ano atrás', other: '${count} anos atrás')}"; - static String m112(name) => "Você e ${name}"; + static String m114(name) => "Você e ${name}"; - static String m113(storageSaved) => + static String m115(storageSaved) => "Você liberou ${storageSaved} com sucesso!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "aNewVersionOfEnteIsAvailable": MessageLookupByLibrary.simpleMessage( - "Uma nova versão do Ente está disponível."), + "Está disponível uma nova versão do Ente."), "about": MessageLookupByLibrary.simpleMessage("Sobre"), "acceptTrustInvite": MessageLookupByLibrary.simpleMessage("Aceitar convite"), "account": MessageLookupByLibrary.simpleMessage("Conta"), - "accountIsAlreadyConfigured": MessageLookupByLibrary.simpleMessage( - "A conta já está configurada."), + "accountIsAlreadyConfigured": + MessageLookupByLibrary.simpleMessage("A conta já está ajustada."), "accountOwnerPersonAppbarTitle": m0, "accountWelcomeBack": - MessageLookupByLibrary.simpleMessage("Bem-vindo(a) de volta!"), + MessageLookupByLibrary.simpleMessage("Bem-vindo de volta!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( - "Eu entendo que se eu perder minha senha, posso perder meus dados, já que meus dados são criptografados de ponta a ponta."), + "Eu entendo que se eu perder a minha palavra-passe, posso perder os meus dados já que esses dados são encriptados de ponta a ponta."), "activeSessions": MessageLookupByLibrary.simpleMessage("Sessões ativas"), "add": MessageLookupByLibrary.simpleMessage("Adicionar"), - "addAName": MessageLookupByLibrary.simpleMessage("Adicione um nome"), + "addAName": MessageLookupByLibrary.simpleMessage("Adiciona um nome"), "addANewEmail": MessageLookupByLibrary.simpleMessage("Adicionar um novo e-mail"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Adicionar colaborador"), - "addCollaborators": m1, "addFiles": MessageLookupByLibrary.simpleMessage("Adicionar arquivos"), - "addFromDevice": - MessageLookupByLibrary.simpleMessage("Adicionar do dispositivo"), - "addItem": m2, + "addFromDevice": MessageLookupByLibrary.simpleMessage( + "Adicionar a partir do dispositivo"), "addLocation": MessageLookupByLibrary.simpleMessage("Adicionar localização"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Adicionar"), @@ -355,22 +329,21 @@ class MessageLookup extends MessageLookupByLibrary { "addNewPerson": MessageLookupByLibrary.simpleMessage("Adicionar nova pessoa"), "addOnPageSubtitle": - MessageLookupByLibrary.simpleMessage("Detalhes dos complementos"), + MessageLookupByLibrary.simpleMessage("Detalhes dos addons"), "addOnValidTill": m3, - "addOns": MessageLookupByLibrary.simpleMessage("Complementos"), + "addOns": MessageLookupByLibrary.simpleMessage("addons"), "addPhotos": MessageLookupByLibrary.simpleMessage("Adicionar fotos"), "addSelected": - MessageLookupByLibrary.simpleMessage("Adicionar selecionado"), + MessageLookupByLibrary.simpleMessage("Adicionar selecionados"), "addToAlbum": MessageLookupByLibrary.simpleMessage("Adicionar ao álbum"), "addToEnte": MessageLookupByLibrary.simpleMessage("Adicionar ao Ente"), "addToHiddenAlbum": - MessageLookupByLibrary.simpleMessage("Adicionar ao álbum oculto"), + MessageLookupByLibrary.simpleMessage("Adicionar a álbum oculto"), "addTrustedContact": MessageLookupByLibrary.simpleMessage("Adicionar contato confiável"), "addViewer": MessageLookupByLibrary.simpleMessage("Adicionar visualizador"), - "addViewers": m4, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Adicione suas fotos agora"), "addedAs": MessageLookupByLibrary.simpleMessage("Adicionado como"), @@ -380,13 +353,15 @@ class MessageLookup extends MessageLookupByLibrary { "Adicionando aos favoritos..."), "admiringThem": m7, "advanced": MessageLookupByLibrary.simpleMessage("Avançado"), - "advancedSettings": MessageLookupByLibrary.simpleMessage("Avançado"), - "after1Day": MessageLookupByLibrary.simpleMessage("Após 1 dia"), - "after1Hour": MessageLookupByLibrary.simpleMessage("Após 1 hora"), - "after1Month": MessageLookupByLibrary.simpleMessage("Após 1 mês"), - "after1Week": MessageLookupByLibrary.simpleMessage("Após 1 semana"), - "after1Year": MessageLookupByLibrary.simpleMessage("Após 1 ano"), - "albumOwner": MessageLookupByLibrary.simpleMessage("Proprietário"), + "advancedSettings": + MessageLookupByLibrary.simpleMessage("Definições avançadas"), + "after1Day": MessageLookupByLibrary.simpleMessage("Depois de 1 dia"), + "after1Hour": MessageLookupByLibrary.simpleMessage("Depois de 1 Hora"), + "after1Month": MessageLookupByLibrary.simpleMessage("Depois de 1 mês"), + "after1Week": + MessageLookupByLibrary.simpleMessage("Depois de 1 semana"), + "after1Year": MessageLookupByLibrary.simpleMessage("Depois de 1 ano"), + "albumOwner": MessageLookupByLibrary.simpleMessage("Dono"), "albumParticipantsCount": m8, "albumTitle": MessageLookupByLibrary.simpleMessage("Título do álbum"), "albumUpdated": @@ -396,12 +371,12 @@ class MessageLookup extends MessageLookupByLibrary { "allMemoriesPreserved": MessageLookupByLibrary.simpleMessage( "Todas as memórias preservadas"), "allPersonGroupingWillReset": MessageLookupByLibrary.simpleMessage( - "Todos os agrupamentos dessa pessoa serão redefinidos, e você perderá todas as sugestões feitas por essa pessoa."), + "Todos os agrupamentos para esta pessoa serão reiniciados e perderá todas as sugestões feitas para esta pessoa"), "allWillShiftRangeBasedOnFirst": MessageLookupByLibrary.simpleMessage( "Este é o primeiro do grupo. As outras fotos selecionadas serão automaticamente alteradas para esta nova data"), "allow": MessageLookupByLibrary.simpleMessage("Permitir"), "allowAddPhotosDescription": MessageLookupByLibrary.simpleMessage( - "Permitir que as pessoas com link também adicionem fotos ao álbum compartilhado."), + "Permitir que pessoas com o link também adicionem fotos ao álbum compartilhado."), "allowAddingPhotos": MessageLookupByLibrary.simpleMessage("Permitir adicionar fotos"), "allowAppToOpenSharedAlbumLinks": MessageLookupByLibrary.simpleMessage( @@ -409,131 +384,126 @@ class MessageLookup extends MessageLookupByLibrary { "allowDownloads": MessageLookupByLibrary.simpleMessage("Permitir downloads"), "allowPeopleToAddPhotos": MessageLookupByLibrary.simpleMessage( - "Permitir que pessoas adicionem fotos"), - "allowPermBody": MessageLookupByLibrary.simpleMessage( - "Permita o acesso a suas fotos das Configurações para que Ente possa exibir e copiar com segurança sua biblioteca."), - "allowPermTitle": - MessageLookupByLibrary.simpleMessage("Permita acesso às Fotos"), + "Permitir que as pessoas adicionem fotos"), "androidBiometricHint": MessageLookupByLibrary.simpleMessage("Verificar identidade"), "androidBiometricNotRecognized": MessageLookupByLibrary.simpleMessage( "Não reconhecido. Tente novamente."), "androidBiometricRequiredTitle": - MessageLookupByLibrary.simpleMessage("Biométrica necessária"), + MessageLookupByLibrary.simpleMessage("Biometria necessária"), "androidBiometricSuccess": MessageLookupByLibrary.simpleMessage("Sucesso"), "androidCancelButton": MessageLookupByLibrary.simpleMessage("Cancelar"), "androidDeviceCredentialsRequiredTitle": - MessageLookupByLibrary.simpleMessage("Credenciais necessários"), + MessageLookupByLibrary.simpleMessage( + "Credenciais do dispositivo são necessárias"), "androidDeviceCredentialsSetupDescription": - MessageLookupByLibrary.simpleMessage("Credenciais necessários"), + MessageLookupByLibrary.simpleMessage( + "Credenciais do dispositivo necessárias"), "androidGoToSettingsDescription": MessageLookupByLibrary.simpleMessage( - "A autenticação biométrica não está definida no dispositivo. Vá em \'Opções > Segurança\' para adicionar a autenticação biométrica."), - "androidIosWebDesktop": MessageLookupByLibrary.simpleMessage( - "Android, iOS, Web, Computador"), + "A autenticação biométrica não está configurada no seu dispositivo. Vá a “Definições > Segurança” para adicionar a autenticação biométrica."), + "androidIosWebDesktop": + MessageLookupByLibrary.simpleMessage("Android, iOS, Web, Desktop"), "androidSignInTitle": MessageLookupByLibrary.simpleMessage("Autenticação necessária"), "appIcon": MessageLookupByLibrary.simpleMessage("Ícone do aplicativo"), - "appLock": - MessageLookupByLibrary.simpleMessage("Bloqueio do aplicativo"), + "appLock": MessageLookupByLibrary.simpleMessage("Bloqueio de app"), "appLockDescriptions": MessageLookupByLibrary.simpleMessage( - "Escolha entre a tela de bloqueio padrão do seu dispositivo e uma tela de bloqueio personalizada com PIN ou senha."), + "Escolha entre o ecrã de bloqueio predefinido do seu dispositivo e um ecrã de bloqueio personalizado com um PIN ou uma palavra-passe."), "appVersion": m9, "appleId": MessageLookupByLibrary.simpleMessage("ID da Apple"), "apply": MessageLookupByLibrary.simpleMessage("Aplicar"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("Aplicar código"), "appstoreSubscription": - MessageLookupByLibrary.simpleMessage("Assinatura da AppStore"), - "archive": MessageLookupByLibrary.simpleMessage("Arquivo"), + MessageLookupByLibrary.simpleMessage("Subscrição da AppStore"), + "archive": MessageLookupByLibrary.simpleMessage("............"), "archiveAlbum": MessageLookupByLibrary.simpleMessage("Arquivar álbum"), - "archiving": MessageLookupByLibrary.simpleMessage("Arquivando..."), + "archiving": MessageLookupByLibrary.simpleMessage("Arquivar..."), "areYouSureThatYouWantToLeaveTheFamily": MessageLookupByLibrary.simpleMessage( - "Você tem certeza que queira sair do plano familiar?"), - "areYouSureYouWantToCancel": - MessageLookupByLibrary.simpleMessage("Deseja cancelar?"), + "Tem certeza que deseja sair do plano familiar?"), + "areYouSureYouWantToCancel": MessageLookupByLibrary.simpleMessage( + "Tem a certeza de que quer cancelar?"), "areYouSureYouWantToChangeYourPlan": - MessageLookupByLibrary.simpleMessage("Deseja trocar de plano?"), + MessageLookupByLibrary.simpleMessage( + "Tem a certeza de que pretende alterar o seu plano?"), "areYouSureYouWantToExit": MessageLookupByLibrary.simpleMessage( - "Tem certeza de que queira sair?"), + "Tem certeza de que deseja sair?"), "areYouSureYouWantToLogout": MessageLookupByLibrary.simpleMessage( - "Você tem certeza que quer encerrar sessão?"), - "areYouSureYouWantToRenew": - MessageLookupByLibrary.simpleMessage("Deseja renovar?"), + "Tem certeza que deseja terminar a sessão?"), + "areYouSureYouWantToRenew": MessageLookupByLibrary.simpleMessage( + "Tem a certeza de que pretende renovar?"), "areYouSureYouWantToResetThisPerson": MessageLookupByLibrary.simpleMessage( - "Deseja redefinir esta pessoa?"), + "Tens a certeza de que queres repor esta pessoa?"), "askCancelReason": MessageLookupByLibrary.simpleMessage( - "Sua assinatura foi cancelada. Deseja compartilhar o motivo?"), + "A sua subscrição foi cancelada. Gostaria de partilhar o motivo?"), "askDeleteReason": MessageLookupByLibrary.simpleMessage( - "Por que você quer excluir sua conta?"), + "Qual o principal motivo pelo qual está a eliminar a conta?"), "askYourLovedOnesToShare": MessageLookupByLibrary.simpleMessage( - "Peça que seus entes queridos compartilhem"), + "Peça aos seus entes queridos para partilharem"), "atAFalloutShelter": MessageLookupByLibrary.simpleMessage("em um abrigo avançado"), "authToChangeEmailVerificationSetting": MessageLookupByLibrary.simpleMessage( - "Autentique-se para alterar o e-mail de verificação"), + "Por favor, autentique-se para alterar a verificação de e-mail"), "authToChangeLockscreenSetting": MessageLookupByLibrary.simpleMessage( - "Autentique para alterar a configuração da tela de bloqueio"), + "Por favor, autentique-se para alterar a configuração da tela do ecrã de bloqueio"), "authToChangeYourEmail": MessageLookupByLibrary.simpleMessage( "Por favor, autentique-se para alterar o seu e-mail"), "authToChangeYourPassword": MessageLookupByLibrary.simpleMessage( - "Autentique para alterar sua senha"), + "Por favor, autentique-se para alterar a palavra-passe"), "authToConfigureTwofactorAuthentication": MessageLookupByLibrary.simpleMessage( - "Autentique para configurar a autenticação de dois fatores"), + "Por favor, autentique para configurar a autenticação de dois fatores"), "authToInitiateAccountDeletion": MessageLookupByLibrary.simpleMessage( - "Autentique para iniciar a exclusão de conta"), + "Autentique-se para iniciar a eliminação da conta"), "authToManageLegacy": MessageLookupByLibrary.simpleMessage( "Autentique-se para gerenciar seus contatos confiáveis"), "authToViewPasskey": MessageLookupByLibrary.simpleMessage( - "Autentique-se para ver sua chave de acesso"), - "authToViewTrashedFiles": MessageLookupByLibrary.simpleMessage( - "Autentique-se para ver seus arquivos excluídos"), + "Autentique-se para ver a sua chave de acesso"), "authToViewYourActiveSessions": MessageLookupByLibrary.simpleMessage( - "Autentique para ver as sessões ativas"), + "Por favor, autentique-se para ver as suas sessões ativas"), "authToViewYourHiddenFiles": MessageLookupByLibrary.simpleMessage( - "Autentique-se para visualizar seus arquivos ocultos"), + "Por favor, autentique para ver seus arquivos ocultos"), "authToViewYourMemories": MessageLookupByLibrary.simpleMessage( - "Autentique-se para ver suas memórias"), + "Por favor, autentique-se para ver suas memórias"), "authToViewYourRecoveryKey": MessageLookupByLibrary.simpleMessage( - "Autentique para ver sua chave de recuperação"), + "Por favor, autentique-se para ver a chave de recuperação"), "authenticating": - MessageLookupByLibrary.simpleMessage("Autenticando..."), + MessageLookupByLibrary.simpleMessage("A Autenticar..."), "authenticationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( - "Falha na autenticação. Tente novamente"), + "Falha na autenticação, por favor tente novamente"), "authenticationSuccessful": - MessageLookupByLibrary.simpleMessage("Autenticado com sucesso!"), + MessageLookupByLibrary.simpleMessage("Autenticação bem sucedida!"), "autoCastDialogBody": MessageLookupByLibrary.simpleMessage( - "Você verá dispositivos de transmissão disponível aqui."), + "Verá os dispositivos Cast disponíveis aqui."), "autoCastiOSPermission": MessageLookupByLibrary.simpleMessage( - "Certifique-se que as permissões da internet local estejam ligadas para o Ente Photos App, em opções."), + "Certifique-se de que as permissões de Rede local estão activadas para a aplicação Ente Photos, nas Definições."), "autoLock": MessageLookupByLibrary.simpleMessage("Bloqueio automático"), "autoLockFeatureDescription": MessageLookupByLibrary.simpleMessage( - "Tempo após o qual o aplicativo bloqueia após ser colocado em segundo plano"), + "Tempo após o qual a aplicação bloqueia depois de ser colocada em segundo plano"), "autoLogoutMessage": MessageLookupByLibrary.simpleMessage( - "Devido ao ocorrido de erros técnicos, você foi desconectado. Pedimos desculpas pela inconveniência."), + "Devido a uma falha técnica, a sua sessão foi encerrada. Pedimos desculpas pelo incómodo."), "autoPair": - MessageLookupByLibrary.simpleMessage("Pareamento automático"), + MessageLookupByLibrary.simpleMessage("Emparelhamento automático"), "autoPairDesc": MessageLookupByLibrary.simpleMessage( - "O pareamento automático só funciona com dispositivos que suportam o Chromecast."), + "O pareamento automático funciona apenas com dispositivos que suportam o Chromecast."), "available": MessageLookupByLibrary.simpleMessage("Disponível"), "availableStorageSpace": m10, "backedUpFolders": MessageLookupByLibrary.simpleMessage( - "Pastas copiadas com segurança"), + "Pastas com cópia de segurança"), "backgroundWithThem": m11, "backup": MessageLookupByLibrary.simpleMessage("Cópia de segurança"), - "backupFailed": MessageLookupByLibrary.simpleMessage( - "Falhou ao copiar com segurança"), + "backupFailed": MessageLookupByLibrary.simpleMessage("Backup falhou"), "backupFile": MessageLookupByLibrary.simpleMessage( "Copiar arquivo com segurança"), "backupOverMobileData": MessageLookupByLibrary.simpleMessage( - "Salvamento com segurança usando dados móveis"), + "Cópia de segurança através dos dados móveis"), "backupSettings": MessageLookupByLibrary.simpleMessage( - "Opções de cópia de segurança"), + "Definições da cópia de segurança"), "backupStatus": MessageLookupByLibrary.simpleMessage( "Status da cópia de segurança"), "backupStatusDescription": MessageLookupByLibrary.simpleMessage( @@ -545,38 +515,16 @@ class MessageLookup extends MessageLookupByLibrary { "blackFridaySale": MessageLookupByLibrary.simpleMessage("Promoção Black Friday"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLBulkEdit": - MessageLookupByLibrary.simpleMessage("Editar todas as datas"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Agora você pode selecionar várias fotos, editar data e hora de todos com um só clique. Alternar datas também são suportados."), - "cLFamilyPlan": MessageLookupByLibrary.simpleMessage( - "Limites de planos familiares"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Agora você pode definir um limite de quanto armazenamento os seus entes queridos podem usar."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Novo Ícone"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Finalmente, um novo ícone para o ente que acreditamos que represente melhor nosso trabalho. Também, adicionamos um alterador de ícone para que você ainda consiga utilizar o ícone antigo."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Memórias"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Redescubra seus momentos especiais - destaque pessoas importantes, suas viagens e celebrações, melhores visitas e muito mais. Ative a aprendizagem automática, mencione si mesmo e seus amigos para melhor experiência."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widgets"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Widgets integrados com memórias já estão disponíveis. Eles apareceram com seus melhores momentos sem precisar abrir o ente."), - "cachedData": - MessageLookupByLibrary.simpleMessage("Dados armazenados em cache"), - "calculating": MessageLookupByLibrary.simpleMessage("Calculando..."), - "canNotOpenBody": MessageLookupByLibrary.simpleMessage( - "Desculpe, este álbum não pode ser aberto no aplicativo."), - "canNotOpenTitle": - MessageLookupByLibrary.simpleMessage("Não pôde abrir este álbum"), + "cachedData": MessageLookupByLibrary.simpleMessage("Dados em cache"), + "calculating": MessageLookupByLibrary.simpleMessage("Calcular..."), "canNotUploadToAlbumsOwnedByOthers": MessageLookupByLibrary.simpleMessage( - "Não é possível enviar para álbuns pertencentes a outros"), + "Não é possível fazer upload para álbuns pertencentes a outros"), "canOnlyCreateLinkForFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( - "Só é possível criar um link para arquivos pertencentes a você"), - "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( - "Só pode remover arquivos de sua propriedade"), + "Só pode criar um link para arquivos pertencentes a você"), + "canOnlyRemoveFilesOwnedByYou": + MessageLookupByLibrary.simpleMessage(""), "cancel": MessageLookupByLibrary.simpleMessage("Cancelar"), "cancelAccountRecovery": MessageLookupByLibrary.simpleMessage("Cancelar recuperação"), @@ -584,69 +532,72 @@ class MessageLookup extends MessageLookupByLibrary { "Deseja mesmo cancelar a recuperação de conta?"), "cancelOtherSubscription": m12, "cancelSubscription": - MessageLookupByLibrary.simpleMessage("Cancelar assinatura"), + MessageLookupByLibrary.simpleMessage("Cancelar subscrição"), "cannotAddMorePhotosAfterBecomingViewer": m13, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( - "Não é possível excluir arquivos compartilhados"), + "Não é possível eliminar ficheiros partilhados"), "castAlbum": MessageLookupByLibrary.simpleMessage("Transferir álbum"), "castIPMismatchBody": MessageLookupByLibrary.simpleMessage( - "Certifique-se de estar na mesma internet que a TV."), + "Certifique-se de estar na mesma rede que a TV."), "castIPMismatchTitle": - MessageLookupByLibrary.simpleMessage("Falhou ao transmitir álbum"), + MessageLookupByLibrary.simpleMessage("Falha ao transmitir álbum"), "castInstruction": MessageLookupByLibrary.simpleMessage( - "Acesse cast.ente.io no dispositivo desejado para parear.\n\nInsira o código abaixo para reproduzir o álbum na sua TV."), + "Visite cast.ente.io no dispositivo que pretende emparelhar.\n\n\nIntroduza o código abaixo para reproduzir o álbum na sua TV."), "centerPoint": MessageLookupByLibrary.simpleMessage("Ponto central"), "change": MessageLookupByLibrary.simpleMessage("Alterar"), "changeEmail": MessageLookupByLibrary.simpleMessage("Alterar e-mail"), "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( "Alterar a localização dos itens selecionados?"), - "changePassword": MessageLookupByLibrary.simpleMessage("Alterar senha"), + "changePassword": + MessageLookupByLibrary.simpleMessage("Alterar palavra-passe"), "changePasswordTitle": - MessageLookupByLibrary.simpleMessage("Alterar senha"), + MessageLookupByLibrary.simpleMessage("Alterar palavra-passe"), "changePermissions": - MessageLookupByLibrary.simpleMessage("Alterar permissões?"), + MessageLookupByLibrary.simpleMessage("Alterar permissões"), "changeYourReferralCode": MessageLookupByLibrary.simpleMessage( - "Alterar código de referência"), + "Alterar o código de referência"), "checkForUpdates": - MessageLookupByLibrary.simpleMessage("Buscar atualizações"), + MessageLookupByLibrary.simpleMessage("Procurar atualizações"), "checkInboxAndSpamFolder": MessageLookupByLibrary.simpleMessage( - "Verifique sua caixa de entrada (e spam) para concluir a verificação"), - "checkStatus": MessageLookupByLibrary.simpleMessage("Verificar estado"), - "checking": MessageLookupByLibrary.simpleMessage("Verificando..."), + "Verifique a sua caixa de entrada (e spam) para concluir a verificação"), + "checkStatus": MessageLookupByLibrary.simpleMessage("Verificar status"), + "checking": MessageLookupByLibrary.simpleMessage("A verificar..."), "checkingModels": - MessageLookupByLibrary.simpleMessage("Verificando modelos..."), + MessageLookupByLibrary.simpleMessage("A verificar modelos..."), "city": MessageLookupByLibrary.simpleMessage("Na cidade"), "claimFreeStorage": MessageLookupByLibrary.simpleMessage( - "Reivindicar armazenamento grátis"), - "claimMore": MessageLookupByLibrary.simpleMessage("Reivindique mais!"), - "claimed": MessageLookupByLibrary.simpleMessage("Reivindicado"), + "Solicitar armazenamento gratuito"), + "claimMore": MessageLookupByLibrary.simpleMessage("Reclamar mais!"), + "claimed": MessageLookupByLibrary.simpleMessage("Reclamado"), "claimedStorageSoFar": m14, "cleanUncategorized": - MessageLookupByLibrary.simpleMessage("Limpar não categorizado"), + MessageLookupByLibrary.simpleMessage("Limpar sem categoria"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( - "Remover todos os arquivos não categorizados que estão presentes em outros álbuns"), + "Remover todos os arquivos da Não Categorizados que estão presentes em outros álbuns"), "clearCaches": MessageLookupByLibrary.simpleMessage("Limpar cache"), "clearIndexes": MessageLookupByLibrary.simpleMessage("Limpar índices"), - "click": MessageLookupByLibrary.simpleMessage("• Clique"), + "click": MessageLookupByLibrary.simpleMessage("Clique"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage("• Clique no menu adicional"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Fechar"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage( "Agrupar por tempo de captura"), - "clubByFileName": - MessageLookupByLibrary.simpleMessage("Agrupar por nome do arquivo"), + "clubByFileName": MessageLookupByLibrary.simpleMessage( + "Agrupar pelo nome de arquivo"), "clusteringProgress": MessageLookupByLibrary.simpleMessage("Progresso de agrupamento"), "codeAppliedPageTitle": MessageLookupByLibrary.simpleMessage("Código aplicado"), "codeChangeLimitReached": MessageLookupByLibrary.simpleMessage( - "Desculpe, você atingiu o limite de mudanças de código."), + "Desculpe, você atingiu o limite de alterações de código."), "codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage( - "Código copiado para a área de transferência"), + "Código copiado para área de transferência"), "codeUsedByYou": MessageLookupByLibrary.simpleMessage("Código usado por você"), "collabLinkSectionDescription": MessageLookupByLibrary.simpleMessage( - "Crie um link para permitir que as pessoas adicionem e vejam fotos no seu álbum compartilhado sem a necessidade do aplicativo ou uma conta Ente. Ótimo para colecionar fotos de eventos."), + "Criar um link para permitir que as pessoas adicionem e visualizem fotos em seu álbum compartilhado sem precisar de um aplicativo Ente ou conta. Ótimo para coletar fotos do evento."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Link colaborativo"), "collaborativeLinkCreatedFor": m15, @@ -657,10 +608,10 @@ class MessageLookup extends MessageLookupByLibrary { "collaboratorsSuccessfullyAdded": m16, "collageLayout": MessageLookupByLibrary.simpleMessage("Layout"), "collageSaved": - MessageLookupByLibrary.simpleMessage("Colagem salva na galeria"), - "collect": MessageLookupByLibrary.simpleMessage("Coletar"), + MessageLookupByLibrary.simpleMessage("Colagem guardada na galeria"), + "collect": MessageLookupByLibrary.simpleMessage("Recolher"), "collectEventPhotos": - MessageLookupByLibrary.simpleMessage("Coletar fotos de evento"), + MessageLookupByLibrary.simpleMessage("Coletar fotos do evento"), "collectPhotos": MessageLookupByLibrary.simpleMessage("Coletar fotos"), "collectPhotosDescription": MessageLookupByLibrary.simpleMessage( "Crie um link onde seus amigos podem enviar fotos na qualidade original."), @@ -668,65 +619,65 @@ class MessageLookup extends MessageLookupByLibrary { "configuration": MessageLookupByLibrary.simpleMessage("Configuração"), "confirm": MessageLookupByLibrary.simpleMessage("Confirmar"), "confirm2FADisable": MessageLookupByLibrary.simpleMessage( - "Você tem certeza que queira desativar a autenticação de dois fatores?"), - "confirmAccountDeletion": - MessageLookupByLibrary.simpleMessage("Confirmar exclusão da conta"), + "Tem a certeza de que pretende desativar a autenticação de dois fatores?"), + "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage( + "Confirmar eliminação de conta"), "confirmAddingTrustedContact": m17, "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( - "Sim, eu quero permanentemente excluir esta conta e os dados em todos os aplicativos."), + "Sim, pretendo apagar permanentemente esta conta e os respetivos dados em todas as aplicações."), "confirmPassword": - MessageLookupByLibrary.simpleMessage("Confirmar senha"), - "confirmPlanChange": - MessageLookupByLibrary.simpleMessage("Confirmar mudança de plano"), + MessageLookupByLibrary.simpleMessage("Confirmar palavra-passe"), + "confirmPlanChange": MessageLookupByLibrary.simpleMessage( + "Confirmar alteração de plano"), "confirmRecoveryKey": MessageLookupByLibrary.simpleMessage( "Confirmar chave de recuperação"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage( - "Confirme sua chave de recuperação"), + "Confirmar chave de recuperação"), "connectToDevice": - MessageLookupByLibrary.simpleMessage("Conectar ao dispositivo"), + MessageLookupByLibrary.simpleMessage("Ligar ao dispositivo"), "contactFamilyAdmin": m18, "contactSupport": - MessageLookupByLibrary.simpleMessage("Contatar suporte"), + MessageLookupByLibrary.simpleMessage("Contactar o suporte"), "contactToManageSubscription": m19, - "contacts": MessageLookupByLibrary.simpleMessage("Contatos"), + "contacts": MessageLookupByLibrary.simpleMessage("Contactos"), "contents": MessageLookupByLibrary.simpleMessage("Conteúdos"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continuar"), - "continueOnFreeTrial": MessageLookupByLibrary.simpleMessage( - "Continuar com a avaliação grátis"), + "continueOnFreeTrial": + MessageLookupByLibrary.simpleMessage("Continuar em teste gratuito"), "convertToAlbum": MessageLookupByLibrary.simpleMessage("Converter para álbum"), "copyEmailAddress": - MessageLookupByLibrary.simpleMessage("Copiar endereço de e-mail"), + MessageLookupByLibrary.simpleMessage("Copiar endereço de email"), "copyLink": MessageLookupByLibrary.simpleMessage("Copiar link"), "copypasteThisCodentoYourAuthenticatorApp": MessageLookupByLibrary.simpleMessage( - "Copie e cole este código\npara o aplicativo autenticador"), + "Copie e cole este código\nno seu aplicativo de autenticação"), "couldNotBackUpTryLater": MessageLookupByLibrary.simpleMessage( - "Nós não podemos copiar com segurança seus dados.\nNós tentaremos novamente mais tarde."), + "Não foi possível fazer o backup de seus dados.\nTentaremos novamente mais tarde."), "couldNotFreeUpSpace": MessageLookupByLibrary.simpleMessage( - "Não foi possível liberar espaço"), + "Não foi possível libertar espaço"), "couldNotUpdateSubscription": MessageLookupByLibrary.simpleMessage( - "Não foi possível atualizar a assinatura"), + "Não foi possível atualizar a subscrição"), "count": MessageLookupByLibrary.simpleMessage("Contagem"), "crashReporting": - MessageLookupByLibrary.simpleMessage("Relatório de erros"), + MessageLookupByLibrary.simpleMessage("Relatório de falhas"), "create": MessageLookupByLibrary.simpleMessage("Criar"), "createAccount": MessageLookupByLibrary.simpleMessage("Criar conta"), "createAlbumActionHint": MessageLookupByLibrary.simpleMessage( - "Pressione para selecionar fotos e clique em + para criar um álbum"), + "Pressione e segure para selecionar fotos e clique em + para criar um álbum"), "createCollaborativeLink": MessageLookupByLibrary.simpleMessage("Criar link colaborativo"), - "createCollage": MessageLookupByLibrary.simpleMessage("Criar colagem"), + "createCollage": MessageLookupByLibrary.simpleMessage("Criar coleção"), "createNewAccount": MessageLookupByLibrary.simpleMessage("Criar nova conta"), "createOrSelectAlbum": MessageLookupByLibrary.simpleMessage("Criar ou selecionar álbum"), "createPublicLink": MessageLookupByLibrary.simpleMessage("Criar link público"), - "creatingLink": MessageLookupByLibrary.simpleMessage("Criando link..."), + "creatingLink": MessageLookupByLibrary.simpleMessage("Criar link..."), "criticalUpdateAvailable": MessageLookupByLibrary.simpleMessage( "Atualização crítica disponível"), - "crop": MessageLookupByLibrary.simpleMessage("Cortar"), + "crop": MessageLookupByLibrary.simpleMessage("Recortar"), "curatedMemories": MessageLookupByLibrary.simpleMessage("Curated memories"), "currentUsageIs": @@ -740,92 +691,90 @@ class MessageLookup extends MessageLookupByLibrary { "dayYesterday": MessageLookupByLibrary.simpleMessage("Ontem"), "declineTrustInvite": MessageLookupByLibrary.simpleMessage("Recusar convite"), - "decrypting": - MessageLookupByLibrary.simpleMessage("Descriptografando..."), + "decrypting": MessageLookupByLibrary.simpleMessage("A desencriptar…"), "decryptingVideo": MessageLookupByLibrary.simpleMessage("Descriptografando vídeo..."), "deduplicateFiles": MessageLookupByLibrary.simpleMessage("Arquivos duplicados"), - "delete": MessageLookupByLibrary.simpleMessage("Excluir"), - "deleteAccount": MessageLookupByLibrary.simpleMessage("Excluir conta"), + "delete": MessageLookupByLibrary.simpleMessage("Apagar"), + "deleteAccount": MessageLookupByLibrary.simpleMessage("Eliminar conta"), "deleteAccountFeedbackPrompt": MessageLookupByLibrary.simpleMessage( - "Lamentamos você ir. Compartilhe seu feedback para nos ajudar a melhorar."), + "Lamentamos a sua partida. Indique-nos a razão para podermos melhorar o serviço."), "deleteAccountPermanentlyButton": MessageLookupByLibrary.simpleMessage( "Excluir conta permanentemente"), - "deleteAlbum": MessageLookupByLibrary.simpleMessage("Excluir álbum"), + "deleteAlbum": MessageLookupByLibrary.simpleMessage("Apagar álbum"), "deleteAlbumDialog": MessageLookupByLibrary.simpleMessage( - "Também excluir as fotos (e vídeos) presentes neste álbum de todos os outros álbuns que eles fazem parte?"), + "Eliminar também as fotos (e vídeos) presentes neste álbum de all os outros álbuns de que fazem parte?"), "deleteAlbumsDialogBody": MessageLookupByLibrary.simpleMessage( - "Isso excluirá todos os álbuns vazios. Isso é útil quando você quiser reduzir a desordem no seu álbum."), - "deleteAll": MessageLookupByLibrary.simpleMessage("Excluir tudo"), + "Esta ação elimina todos os álbuns vazios. Isto é útil quando pretende reduzir a confusão na sua lista de álbuns."), + "deleteAll": MessageLookupByLibrary.simpleMessage("Apagar tudo"), "deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage( - "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."), + "Esta conta está ligada a outras aplicações Ente, se utilizar alguma. Os seus dados carregados, em todas as aplicações Ente, serão agendados para eliminação e a sua conta será permanentemente eliminada."), "deleteEmailRequest": MessageLookupByLibrary.simpleMessage( - "Por favor, envie um e-mail a account-deletion@ente.io do seu endereço de e-mail registrado."), + "Envie um e-mail para accountt-deletion@ente.io a partir do seu endereço de email registrado."), "deleteEmptyAlbums": - MessageLookupByLibrary.simpleMessage("Excluir álbuns vazios"), + MessageLookupByLibrary.simpleMessage("Apagar álbuns vazios"), "deleteEmptyAlbumsWithQuestionMark": - MessageLookupByLibrary.simpleMessage("Excluir álbuns vazios?"), + MessageLookupByLibrary.simpleMessage("Apagar álbuns vazios?"), "deleteFromBoth": - MessageLookupByLibrary.simpleMessage("Excluir de ambos"), + MessageLookupByLibrary.simpleMessage("Apagar de ambos"), "deleteFromDevice": - MessageLookupByLibrary.simpleMessage("Excluir do dispositivo"), + MessageLookupByLibrary.simpleMessage("Apagar do dispositivo"), "deleteFromEnte": - MessageLookupByLibrary.simpleMessage("Excluir do Ente"), + MessageLookupByLibrary.simpleMessage("Apagar do Ente"), "deleteItemCount": m21, "deleteLocation": - MessageLookupByLibrary.simpleMessage("Excluir localização"), - "deletePhotos": MessageLookupByLibrary.simpleMessage("Excluir fotos"), + MessageLookupByLibrary.simpleMessage("Apagar localização"), + "deletePhotos": MessageLookupByLibrary.simpleMessage("Apagar fotos"), "deleteProgress": m22, "deleteReason1": MessageLookupByLibrary.simpleMessage( - "Está faltando um recurso-chave que eu preciso"), + "Falta uma funcionalidade-chave de que eu necessito"), "deleteReason2": MessageLookupByLibrary.simpleMessage( - "O aplicativo ou um certo recurso não funciona da maneira que eu acredito que deveria funcionar"), + "O aplicativo ou um determinado recurso não se comportou como era suposto"), "deleteReason3": MessageLookupByLibrary.simpleMessage( - "Encontrei outro serviço que considero melhor"), + "Encontrei outro serviço de que gosto mais"), "deleteReason4": - MessageLookupByLibrary.simpleMessage("Meu motivo não está listado"), + MessageLookupByLibrary.simpleMessage("O motivo não está na lista"), "deleteRequestSLAText": MessageLookupByLibrary.simpleMessage( - "Sua solicitação será revisada em até 72 horas."), + "O seu pedido será processado dentro de 72 horas."), "deleteSharedAlbum": MessageLookupByLibrary.simpleMessage( "Excluir álbum compartilhado?"), "deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage( - "O álbum será apagado para todos\n\nVocê perderá o acesso a fotos compartilhadas neste álbum que pertencem aos outros"), - "deselectAll": - MessageLookupByLibrary.simpleMessage("Deselecionar tudo"), + "O álbum será apagado para todos\n\nVocê perderá o acesso a fotos compartilhadas neste álbum que são propriedade de outros"), + "deselectAll": MessageLookupByLibrary.simpleMessage("Desmarcar tudo"), "designedToOutlive": MessageLookupByLibrary.simpleMessage("Feito para ter longevidade"), "details": MessageLookupByLibrary.simpleMessage("Detalhes"), "developerSettings": - MessageLookupByLibrary.simpleMessage("Opções de desenvolvedor"), + MessageLookupByLibrary.simpleMessage("Definições do programador"), "developerSettingsWarning": MessageLookupByLibrary.simpleMessage( - "Deseja modificar as Opções de Desenvolvedor?"), + "Tem a certeza de que pretende modificar as definições de programador?"), "deviceCodeHint": - MessageLookupByLibrary.simpleMessage("Insira o código"), + MessageLookupByLibrary.simpleMessage("Introduza o código"), "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage( - "Arquivos adicionados ao álbum do dispositivo serão automaticamente enviados para o Ente."), + "Os ficheiros adicionados a este álbum de dispositivo serão automaticamente transferidos para o Ente."), "deviceLock": MessageLookupByLibrary.simpleMessage("Bloqueio do dispositivo"), "deviceLockExplanation": MessageLookupByLibrary.simpleMessage( - "Desativa o bloqueio de tela quando o Ente está de fundo e têm uma cópia de segurança sendo feita. Isso normalmente não é necessário, no entanto, ajuda a envios grandes e importações iniciais de bibliotecas maiores concluírem mais rápido."), + "Desativar o bloqueio do ecrã do dispositivo quando o Ente estiver em primeiro plano e houver uma cópia de segurança em curso. Normalmente, isto não é necessário, mas pode ajudar a que os grandes carregamentos e as importações iniciais de grandes bibliotecas sejam concluídos mais rapidamente."), "deviceNotFound": MessageLookupByLibrary.simpleMessage("Dispositivo não encontrado"), "didYouKnow": MessageLookupByLibrary.simpleMessage("Você sabia?"), "disableAutoLock": MessageLookupByLibrary.simpleMessage( "Desativar bloqueio automático"), "disableDownloadWarningBody": MessageLookupByLibrary.simpleMessage( - "Os visualizadores podem fazer capturas de tela ou salvar uma cópia de suas fotos usando ferramentas externas"), + "Visualizadores ainda podem fazer capturas de tela ou salvar uma cópia das suas fotos usando ferramentas externas"), "disableDownloadWarningTitle": - MessageLookupByLibrary.simpleMessage("Por favor, saiba que"), + MessageLookupByLibrary.simpleMessage("Por favor, observe"), "disableLinkMessage": m23, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Desativar autenticação de dois fatores"), "disablingTwofactorAuthentication": MessageLookupByLibrary.simpleMessage( - "Desativando a autenticação de dois fatores..."), + "Desativar a autenticação de dois factores..."), "discord": MessageLookupByLibrary.simpleMessage("Discord"), - "discover": MessageLookupByLibrary.simpleMessage("Explorar"), - "discover_babies": MessageLookupByLibrary.simpleMessage("Bebês"), + "discover": MessageLookupByLibrary.simpleMessage("Descobrir"), + "discover_babies": MessageLookupByLibrary.simpleMessage("Bebés"), "discover_celebrations": MessageLookupByLibrary.simpleMessage("Comemorações"), "discover_food": MessageLookupByLibrary.simpleMessage("Comida"), @@ -838,29 +787,30 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Animais de estimação"), "discover_receipts": MessageLookupByLibrary.simpleMessage("Recibos"), "discover_screenshots": - MessageLookupByLibrary.simpleMessage("Capturas de tela"), + MessageLookupByLibrary.simpleMessage("Capturas de ecrã"), "discover_selfies": MessageLookupByLibrary.simpleMessage("Selfies"), "discover_sunset": MessageLookupByLibrary.simpleMessage("Pôr do sol"), "discover_visiting_cards": MessageLookupByLibrary.simpleMessage("Cartões de visita"), "discover_wallpapers": MessageLookupByLibrary.simpleMessage("Papéis de parede"), - "dismiss": MessageLookupByLibrary.simpleMessage("Descartar"), + "dismiss": MessageLookupByLibrary.simpleMessage("Rejeitar"), "distanceInKMUnit": MessageLookupByLibrary.simpleMessage("km"), - "doNotSignOut": MessageLookupByLibrary.simpleMessage("Não sair"), + "doNotSignOut": + MessageLookupByLibrary.simpleMessage("Não terminar a sessão"), "doThisLater": - MessageLookupByLibrary.simpleMessage("Fazer isso depois"), + MessageLookupByLibrary.simpleMessage("Fazer isto mais tarde"), "doYouWantToDiscardTheEditsYouHaveMade": MessageLookupByLibrary.simpleMessage( - "Você quer descartar as edições que você fez?"), + "Pretende eliminar as edições que efectuou?"), "done": MessageLookupByLibrary.simpleMessage("Concluído"), "dontSave": MessageLookupByLibrary.simpleMessage("Não salvar"), - "doubleYourStorage": - MessageLookupByLibrary.simpleMessage("Duplique seu armazenamento"), - "download": MessageLookupByLibrary.simpleMessage("Baixar"), + "doubleYourStorage": MessageLookupByLibrary.simpleMessage( + "Duplicar o seu armazenamento"), + "download": MessageLookupByLibrary.simpleMessage("Download"), "downloadFailed": - MessageLookupByLibrary.simpleMessage("Falhou ao baixar"), - "downloading": MessageLookupByLibrary.simpleMessage("Baixando..."), + MessageLookupByLibrary.simpleMessage("Falha no download"), + "downloading": MessageLookupByLibrary.simpleMessage("A transferir..."), "dropSupportEmail": m24, "duplicateFileCountWithStorageSaved": m25, "duplicateItemsGroup": m26, @@ -871,103 +821,97 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Editar localização"), "editPerson": MessageLookupByLibrary.simpleMessage("Editar pessoa"), "editTime": MessageLookupByLibrary.simpleMessage("Editar tempo"), - "editsSaved": MessageLookupByLibrary.simpleMessage("Edições salvas"), + "editsSaved": MessageLookupByLibrary.simpleMessage("Edição guardada"), "editsToLocationWillOnlyBeSeenWithinEnte": MessageLookupByLibrary.simpleMessage( - "Edições à localização serão apenas vistos no Ente"), + "Edições para localização só serão vistas dentro do Ente"), "eligible": MessageLookupByLibrary.simpleMessage("elegível"), - "email": MessageLookupByLibrary.simpleMessage("E-mail"), - "emailAlreadyRegistered": - MessageLookupByLibrary.simpleMessage("E-mail já registrado."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, - "emailNotRegistered": - MessageLookupByLibrary.simpleMessage("E-mail não registrado."), + "email": MessageLookupByLibrary.simpleMessage("Email"), + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Verificação por e-mail"), "emailYourLogs": - MessageLookupByLibrary.simpleMessage("Enviar registros por e-mail"), - "embracingThem": m30, + MessageLookupByLibrary.simpleMessage("Enviar logs por e-mail"), + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Contatos de emergência"), "empty": MessageLookupByLibrary.simpleMessage("Esvaziar"), - "emptyTrash": - MessageLookupByLibrary.simpleMessage("Esvaziar a lixeira?"), + "emptyTrash": MessageLookupByLibrary.simpleMessage("Esvaziar lixo?"), "enable": MessageLookupByLibrary.simpleMessage("Ativar"), "enableMLIndexingDesc": MessageLookupByLibrary.simpleMessage( - "Ente suporta aprendizagem de máquina para reconhecimento facial, busca mágica e outros recursos de busca avançados"), + "O Ente suporta a aprendizagem automática no dispositivo para reconhecimento facial, pesquisa mágica e outras funcionalidades de pesquisa avançadas"), "enableMachineLearningBanner": MessageLookupByLibrary.simpleMessage( - "Ativar aprendizagem de máquina para busca mágica e reconhecimento facial"), + "Habilitar aprendizagem automática para pesquisa mágica e reconhecimento de rosto"), "enableMaps": MessageLookupByLibrary.simpleMessage("Ativar mapas"), "enableMapsDesc": MessageLookupByLibrary.simpleMessage( - "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."), + "Esta opção mostra as suas fotografias num mapa do mundo.\n\n\nEste mapa é alojado pelo Open Street Map e as localizações exactas das suas fotografias nunca são partilhadas.\n\n\nPode desativar esta funcionalidade em qualquer altura nas Definições."), "enabled": MessageLookupByLibrary.simpleMessage("Ativado"), - "encryptingBackup": MessageLookupByLibrary.simpleMessage( - "Criptografando cópia de segurança..."), - "encryption": MessageLookupByLibrary.simpleMessage("Criptografia"), + "encryptingBackup": + MessageLookupByLibrary.simpleMessage("Criptografando backup..."), + "encryption": MessageLookupByLibrary.simpleMessage("Encriptação"), "encryptionKeys": - MessageLookupByLibrary.simpleMessage("Chaves de criptografia"), + MessageLookupByLibrary.simpleMessage("Chaves de encriptação"), "endpointUpdatedMessage": MessageLookupByLibrary.simpleMessage( - "Ponto final atualizado com sucesso"), + "Endpoint atualizado com sucesso"), "endtoendEncryptedByDefault": MessageLookupByLibrary.simpleMessage( - "Criptografado de ponta a ponta por padrão"), + "Criptografia de ponta a ponta por padrão"), "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": MessageLookupByLibrary.simpleMessage( "Ente pode criptografar e preservar arquivos apenas se você conceder acesso a eles"), "entePhotosPerm": MessageLookupByLibrary.simpleMessage( "Ente precisa de permissão para preservar suas fotos"), "enteSubscriptionPitch": MessageLookupByLibrary.simpleMessage( - "O Ente preserva suas memórias, então eles sempre estão disponíveis para você, mesmo se você perder o dispositivo."), + "O Ente preserva as suas memórias, para que estejam sempre disponíveis, mesmo que perca o seu dispositivo."), "enteSubscriptionShareWithFamily": MessageLookupByLibrary.simpleMessage( "Sua família também pode ser adicionada ao seu plano."), "enterAlbumName": - MessageLookupByLibrary.simpleMessage("Inserir nome do álbum"), + MessageLookupByLibrary.simpleMessage("Introduzir nome do álbum"), "enterCode": MessageLookupByLibrary.simpleMessage("Insira o código"), "enterCodeDescription": MessageLookupByLibrary.simpleMessage( - "Insira o código fornecido pelo seu amigo para reivindicar o armazenamento grátis para os dois"), + "Introduza o código fornecido pelo seu amigo para obter armazenamento gratuito para ambos"), "enterDateOfBirth": MessageLookupByLibrary.simpleMessage("Aniversário (opcional)"), - "enterEmail": MessageLookupByLibrary.simpleMessage("Inserir e-mail"), + "enterEmail": MessageLookupByLibrary.simpleMessage("Digite o e-mail"), "enterFileName": MessageLookupByLibrary.simpleMessage("Inserir nome do arquivo"), "enterName": MessageLookupByLibrary.simpleMessage("Inserir nome"), "enterNewPasswordToEncrypt": MessageLookupByLibrary.simpleMessage( - "Insira uma senha nova para criptografar seus dados"), - "enterPassword": MessageLookupByLibrary.simpleMessage("Inserir senha"), + "Inserir uma nova palavra-passe para encriptar os seus dados"), + "enterPassword": + MessageLookupByLibrary.simpleMessage("Introduzir palavra-passe"), "enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage( - "Insira uma senha que podemos usar para criptografar seus dados"), + "Inserir uma palavra-passe para encriptar os seus dados"), "enterPersonName": MessageLookupByLibrary.simpleMessage("Inserir nome da pessoa"), - "enterPin": MessageLookupByLibrary.simpleMessage("Inserir PIN"), + "enterPin": MessageLookupByLibrary.simpleMessage("Introduzir PIN"), "enterReferralCode": MessageLookupByLibrary.simpleMessage( - "Inserir código de referência"), + "Insira o código de referência"), "enterThe6digitCodeFromnyourAuthenticatorApp": MessageLookupByLibrary.simpleMessage( - "Digite o código de 6 dígitos do\naplicativo de autenticador"), + "Introduzir o código de 6 dígitos da\nsua aplicação de autenticação"), "enterValidEmail": MessageLookupByLibrary.simpleMessage( - "Insira um endereço de e-mail válido."), + "Por favor, insira um endereço de email válido."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage( - "Insira seu endereço de e-mail"), - "enterYourPassword": - MessageLookupByLibrary.simpleMessage("Insira sua senha"), + "Insira o seu endereço de email"), + "enterYourPassword": MessageLookupByLibrary.simpleMessage( + "Introduza a sua palavra-passe"), "enterYourRecoveryKey": MessageLookupByLibrary.simpleMessage( - "Insira sua chave de recuperação"), + "Insira a sua chave de recuperação"), "error": MessageLookupByLibrary.simpleMessage("Erro"), - "everywhere": - MessageLookupByLibrary.simpleMessage("em todas as partes"), + "everywhere": MessageLookupByLibrary.simpleMessage("em todo o lado"), "exif": MessageLookupByLibrary.simpleMessage("EXIF"), "existingUser": - MessageLookupByLibrary.simpleMessage("Usuário existente"), + MessageLookupByLibrary.simpleMessage("Utilizador existente"), "expiredLinkInfo": MessageLookupByLibrary.simpleMessage( - "O link expirou. Selecione um novo tempo de expiração ou desative a expiração do link."), - "exportLogs": - MessageLookupByLibrary.simpleMessage("Exportar registros"), + "Este link expirou. Por favor, selecione um novo tempo de expiração ou desabilite a expiração do link."), + "exportLogs": MessageLookupByLibrary.simpleMessage("Exportar logs"), "exportYourData": - MessageLookupByLibrary.simpleMessage("Exportar dados"), + MessageLookupByLibrary.simpleMessage("Exportar os seus dados"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Fotos adicionais encontradas"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Rosto não agrupado ainda, volte aqui mais tarde"), "faceRecognition": @@ -975,142 +919,141 @@ class MessageLookup extends MessageLookupByLibrary { "faces": MessageLookupByLibrary.simpleMessage("Rostos"), "failed": MessageLookupByLibrary.simpleMessage("Falhou"), "failedToApplyCode": - MessageLookupByLibrary.simpleMessage("Falhou ao aplicar código"), + MessageLookupByLibrary.simpleMessage("Falha ao aplicar código"), "failedToCancel": MessageLookupByLibrary.simpleMessage("Falhou ao cancelar"), - "failedToDownloadVideo": - MessageLookupByLibrary.simpleMessage("Falhou ao baixar vídeo"), + "failedToDownloadVideo": MessageLookupByLibrary.simpleMessage( + "Falha ao fazer o download do vídeo"), "failedToFetchActiveSessions": MessageLookupByLibrary.simpleMessage( - "Falhou ao obter sessões ativas"), + "Falha ao obter sessões em atividade"), "failedToFetchOriginalForEdit": MessageLookupByLibrary.simpleMessage( - "Falhou ao obter original para edição"), + "Falha ao obter original para edição"), "failedToFetchReferralDetails": MessageLookupByLibrary.simpleMessage( - "Não foi possível buscar os detalhes de referência. Tente novamente mais tarde."), + "Não foi possível obter detalhes de indicação. Por favor, tente novamente mais tarde."), "failedToLoadAlbums": - MessageLookupByLibrary.simpleMessage("Falhou ao carregar álbuns"), - "failedToPlayVideo": - MessageLookupByLibrary.simpleMessage("Falhou ao reproduzir vídeo"), + MessageLookupByLibrary.simpleMessage("Falha ao carregar álbuns"), + "failedToPlayVideo": MessageLookupByLibrary.simpleMessage( + "Falha ao reproduzir multimédia"), "failedToRefreshStripeSubscription": MessageLookupByLibrary.simpleMessage( - "Falhou ao atualizar assinatura"), + "Falha ao atualizar subscrição"), "failedToRenew": MessageLookupByLibrary.simpleMessage("Falhou ao renovar"), "failedToVerifyPaymentStatus": MessageLookupByLibrary.simpleMessage( - "Falhou ao verificar estado do pagamento"), + "Falha ao verificar status do pagamento"), "familyPlanOverview": MessageLookupByLibrary.simpleMessage( - "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!"), + "Adicione 5 membros da família ao seu plano existente sem pagar mais.\n\n\nCada membro tem o seu próprio espaço privado e não pode ver os ficheiros dos outros, a menos que sejam partilhados.\n\n\nOs planos familiares estão disponíveis para clientes que tenham uma subscrição paga do Ente.\n\n\nSubscreva agora para começar!"), "familyPlanPortalTitle": MessageLookupByLibrary.simpleMessage("Família"), "familyPlans": MessageLookupByLibrary.simpleMessage("Planos familiares"), - "faq": MessageLookupByLibrary.simpleMessage("Perguntas frequentes"), + "faq": MessageLookupByLibrary.simpleMessage("Perguntas Frequentes"), "faqs": MessageLookupByLibrary.simpleMessage("Perguntas frequentes"), "favorite": MessageLookupByLibrary.simpleMessage("Favorito"), - "feastingWithThem": m32, - "feedback": MessageLookupByLibrary.simpleMessage("Feedback"), + "feastingWithThem": m33, + "feedback": MessageLookupByLibrary.simpleMessage("Opinião"), "file": MessageLookupByLibrary.simpleMessage("Arquivo"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( - "Falhou ao salvar arquivo na galeria"), + "Falha ao guardar o ficheiro na galeria"), "fileInfoAddDescHint": - MessageLookupByLibrary.simpleMessage("Adicionar descrição..."), + MessageLookupByLibrary.simpleMessage("Acrescente uma descrição..."), "fileNotUploadedYet": MessageLookupByLibrary.simpleMessage("Arquivo ainda não enviado"), "fileSavedToGallery": - MessageLookupByLibrary.simpleMessage("Arquivo salvo na galeria"), + MessageLookupByLibrary.simpleMessage("Arquivo guardado na galeria"), "fileTypes": MessageLookupByLibrary.simpleMessage("Tipos de arquivo"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": - MessageLookupByLibrary.simpleMessage("Arquivos excluídos"), - "filesSavedToGallery": - MessageLookupByLibrary.simpleMessage("Arquivos salvos na galeria"), + MessageLookupByLibrary.simpleMessage("Arquivos apagados"), + "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( + "Arquivos guardados na galeria"), "findPeopleByName": MessageLookupByLibrary.simpleMessage( - "Busque pessoas facilmente pelo nome"), + "Encontrar pessoas rapidamente pelo nome"), "findThemQuickly": - MessageLookupByLibrary.simpleMessage("Busque-os rapidamente"), + MessageLookupByLibrary.simpleMessage("Ache-os rapidamente"), "flip": MessageLookupByLibrary.simpleMessage("Inverter"), "food": MessageLookupByLibrary.simpleMessage("Prazer em culinária"), "forYourMemories": MessageLookupByLibrary.simpleMessage("para suas memórias"), - "forgotPassword": - MessageLookupByLibrary.simpleMessage("Esqueci a senha"), + "forgotPassword": MessageLookupByLibrary.simpleMessage( + "Esqueceu-se da palavra-passe"), "foundFaces": MessageLookupByLibrary.simpleMessage("Rostos encontrados"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( - "Armazenamento grátis reivindicado"), - "freeStorageOnReferralSuccess": m35, - "freeStorageUsable": - MessageLookupByLibrary.simpleMessage("Armazenamento disponível"), - "freeTrial": MessageLookupByLibrary.simpleMessage("Avaliação grátis"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "Armazenamento gratuito reclamado"), + "freeStorageOnReferralSuccess": m36, + "freeStorageUsable": MessageLookupByLibrary.simpleMessage( + "Armazenamento livre utilizável"), + "freeTrial": MessageLookupByLibrary.simpleMessage("Teste grátis"), + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( - "Liberar espaço no dispositivo"), + "Libertar espaço no dispositivo"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( - "Economize espaço em seu dispositivo por limpar arquivos já salvos com segurança."), - "freeUpSpace": MessageLookupByLibrary.simpleMessage("Liberar espaço"), - "freeUpSpaceSaving": m39, + "Poupe espaço no seu dispositivo limpando ficheiros dos quais já foi feita uma cópia de segurança."), + "freeUpSpace": MessageLookupByLibrary.simpleMessage("Libertar espaço"), "gallery": MessageLookupByLibrary.simpleMessage("Galeria"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( - "Até 1.000 memórias exibidas na galeria"), + "Até 1000 memórias mostradas na galeria"), "general": MessageLookupByLibrary.simpleMessage("Geral"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( - "Gerando chaves de criptografia..."), - "genericProgress": m40, - "goToSettings": MessageLookupByLibrary.simpleMessage("Ir às opções"), + "Gerando chaves de encriptação..."), + "genericProgress": m41, + "goToSettings": + MessageLookupByLibrary.simpleMessage("Ir para as definições"), "googlePlayId": MessageLookupByLibrary.simpleMessage("ID do Google Play"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( - "Permita o acesso a todas as fotos nas opções do aplicativo"), + "Por favor, permita o acesso a todas as fotos nas definições do aplicativo"), "grantPermission": - MessageLookupByLibrary.simpleMessage("Conceder permissões"), + MessageLookupByLibrary.simpleMessage("Conceder permissão"), "greenery": MessageLookupByLibrary.simpleMessage("A vegetação verde"), "groupNearbyPhotos": MessageLookupByLibrary.simpleMessage("Agrupar fotos próximas"), - "guestView": MessageLookupByLibrary.simpleMessage("Vista do convidado"), + "guestView": MessageLookupByLibrary.simpleMessage("Visão de convidado"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( - "Para ativar a vista do convidado, defina uma senha de acesso no dispositivo ou bloqueie sua tela nas opções do sistema."), + "Para ativar a vista de convidado, configure o código de acesso do dispositivo ou o bloqueio do ecrã nas definições do sistema."), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( - "Não rastreamos instalações de aplicativo. Seria útil se você contasse onde nos encontrou!"), + "Não monitorizamos as instalações de aplicações. Ajudaria se nos dissesse onde nos encontrou!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( - "Como você soube do Ente? (opcional)"), + "Como é que soube do Ente? (opcional)"), "help": MessageLookupByLibrary.simpleMessage("Ajuda"), "hidden": MessageLookupByLibrary.simpleMessage("Oculto"), "hide": MessageLookupByLibrary.simpleMessage("Ocultar"), "hideContent": MessageLookupByLibrary.simpleMessage("Ocultar conteúdo"), "hideContentDescriptionAndroid": MessageLookupByLibrary.simpleMessage( - "Oculta os conteúdos do aplicativo no seletor de aplicativos e desativa capturas de tela"), + "Oculta o conteúdo da aplicação no alternador de aplicações e desactiva as capturas de ecrã"), "hideContentDescriptionIos": MessageLookupByLibrary.simpleMessage( - "Oculta o conteúdo no seletor de aplicativos"), + "Oculta o conteúdo da aplicação no alternador de aplicações"), "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Ocultar itens compartilhados da galeria inicial"), "hiding": MessageLookupByLibrary.simpleMessage("Ocultando..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": - MessageLookupByLibrary.simpleMessage("Hospedado em OSM France"), + MessageLookupByLibrary.simpleMessage("Hospedado na OSM France"), "howItWorks": MessageLookupByLibrary.simpleMessage("Como funciona"), "howToViewShareeVerificationID": MessageLookupByLibrary.simpleMessage( - "Peça-os para manterem pressionado no endereço de e-mail na tela de opções, e verifique-se os IDs de ambos os dispositivos correspondem."), + "Por favor, peça-lhes para pressionar longamente o endereço de e-mail na tela de configurações e verifique se os IDs de ambos os dispositivos coincidem."), "iOSGoToSettingsDescription": MessageLookupByLibrary.simpleMessage( - "A autenticação biométrica não está definida no dispositivo. Ative o Touch ID ou Face ID no dispositivo."), + "A autenticação biométrica não está configurada no seu dispositivo. Active o Touch ID ou o Face ID no seu telemóvel."), "iOSLockOut": MessageLookupByLibrary.simpleMessage( - "A autenticação biométrica está desativada. Bloqueie e desbloqueie sua tela para ativá-la."), + "A autenticação biométrica está desativada. Por favor, bloqueie e desbloqueie o ecrã para ativá-la."), "iOSOkButton": MessageLookupByLibrary.simpleMessage("OK"), "ignoreUpdate": MessageLookupByLibrary.simpleMessage("Ignorar"), "ignored": MessageLookupByLibrary.simpleMessage("ignorado"), "ignoredFolderUploadReason": MessageLookupByLibrary.simpleMessage( - "Alguns arquivos neste álbum são ignorados do envio porque eles foram anteriormente excluídos do Ente."), + "Alguns ficheiros deste álbum não podem ser carregados porque foram anteriormente eliminados do Ente."), "imageNotAnalyzed": MessageLookupByLibrary.simpleMessage("Imagem não analisada"), "immediately": MessageLookupByLibrary.simpleMessage("Imediatamente"), - "importing": MessageLookupByLibrary.simpleMessage("Importando...."), + "importing": MessageLookupByLibrary.simpleMessage("A importar..."), "incorrectCode": - MessageLookupByLibrary.simpleMessage("Código incorreto"), + MessageLookupByLibrary.simpleMessage("Código incorrecto"), "incorrectPasswordTitle": - MessageLookupByLibrary.simpleMessage("Senha incorreta"), + MessageLookupByLibrary.simpleMessage("Palavra-passe incorreta"), "incorrectRecoveryKey": MessageLookupByLibrary.simpleMessage( "Chave de recuperação incorreta"), "incorrectRecoveryKeyBody": MessageLookupByLibrary.simpleMessage( @@ -1119,7 +1062,7 @@ class MessageLookup extends MessageLookupByLibrary { "Chave de recuperação incorreta"), "indexedItems": MessageLookupByLibrary.simpleMessage("Itens indexados"), "indexingIsPaused": MessageLookupByLibrary.simpleMessage( - "A indexação parou, ela será retomada automaticamente quando o dispositivo estiver pronto."), + "A indexação está pausada, será retomada automaticamente quando o dispositivo estiver pronto."), "ineligible": MessageLookupByLibrary.simpleMessage("Inelegível"), "info": MessageLookupByLibrary.simpleMessage("Info"), "insecureDevice": @@ -1127,28 +1070,28 @@ class MessageLookup extends MessageLookupByLibrary { "installManually": MessageLookupByLibrary.simpleMessage("Instalar manualmente"), "invalidEmailAddress": - MessageLookupByLibrary.simpleMessage("Endereço de e-mail inválido"), + MessageLookupByLibrary.simpleMessage("Endereço de email inválido"), "invalidEndpoint": - MessageLookupByLibrary.simpleMessage("Ponto final inválido"), + MessageLookupByLibrary.simpleMessage("Endpoint inválido"), "invalidEndpointMessage": MessageLookupByLibrary.simpleMessage( - "Desculpe, o ponto final inserido é inválido. Insira um ponto final válido e tente novamente."), + "Desculpe, o endpoint que introduziu é inválido. Introduza um ponto final válido e tente novamente."), "invalidKey": MessageLookupByLibrary.simpleMessage("Chave inválida"), "invalidRecoveryKey": MessageLookupByLibrary.simpleMessage( - "A chave de recuperação que você inseriu não é válida. Certifique-se de conter 24 caracteres, e verifique a ortografia de cada um deles.\n\nSe você inseriu um código de recuperação mais antigo, verifique se ele tem 64 caracteres e verifique cada um deles."), + "A chave de recuperação que inseriu não é válida. Por favor, certifique-se que ela contém 24 palavras e verifique a ortografia de cada uma.\n\nSe inseriu um código de recuperação mais antigo, certifique-se de que tem 64 caracteres e verifique cada um deles."), "invite": MessageLookupByLibrary.simpleMessage("Convidar"), "inviteToEnte": - MessageLookupByLibrary.simpleMessage("Convidar ao Ente"), + MessageLookupByLibrary.simpleMessage("Convidar para Ente"), "inviteYourFriends": - MessageLookupByLibrary.simpleMessage("Convide seus amigos"), - "inviteYourFriendsToEnte": - MessageLookupByLibrary.simpleMessage("Convide seus amigos ao Ente"), + MessageLookupByLibrary.simpleMessage("Convide os seus amigos"), + "inviteYourFriendsToEnte": MessageLookupByLibrary.simpleMessage( + "Convide seus amigos para o Ente"), "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( - "Parece que algo deu errado. Tente novamente mais tarde. Caso o erro persistir, por favor, entre em contato com nossa equipe."), - "itemCount": m42, + "Parece que algo correu mal. Por favor, tente novamente após algum tempo. Se o erro persistir, contacte a nossa equipa de apoio."), + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( - "Os itens exibem o número de dias restantes antes da exclusão permanente"), + "Os itens mostram o número de dias restantes antes da eliminação permanente"), "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Os itens selecionados serão removidos deste álbum"), "join": MessageLookupByLibrary.simpleMessage("Unir-se"), @@ -1160,13 +1103,13 @@ class MessageLookup extends MessageLookupByLibrary { "joinAlbumSubtextViewer": MessageLookupByLibrary.simpleMessage( "para adicionar isso aos álbuns compartilhados"), "joinDiscord": - MessageLookupByLibrary.simpleMessage("Junte-se ao Discord"), + MessageLookupByLibrary.simpleMessage("Juntar-se ao Discord"), "keepPhotos": MessageLookupByLibrary.simpleMessage("Manter fotos"), "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"), "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( - "Ajude-nos com esta informação"), + "Por favor, ajude-nos com esta informação"), "language": MessageLookupByLibrary.simpleMessage("Idioma"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Última atualização"), "lastYearsTrip": @@ -1174,57 +1117,56 @@ class MessageLookup extends MessageLookupByLibrary { "leave": MessageLookupByLibrary.simpleMessage("Sair"), "leaveAlbum": MessageLookupByLibrary.simpleMessage("Sair do álbum"), "leaveFamily": - MessageLookupByLibrary.simpleMessage("Sair do plano familiar"), + MessageLookupByLibrary.simpleMessage("Deixar plano famíliar"), "leaveSharedAlbum": MessageLookupByLibrary.simpleMessage( "Sair do álbum compartilhado?"), "left": MessageLookupByLibrary.simpleMessage("Esquerda"), "legacy": MessageLookupByLibrary.simpleMessage("Legado"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Contas legadas"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "O legado permite que contatos confiáveis acessem sua conta em sua ausência."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( "Contatos confiáveis podem iniciar recuperação de conta. Se não cancelado dentro de 30 dias, redefina sua senha e acesse sua conta."), - "light": MessageLookupByLibrary.simpleMessage("Brilho"), + "light": MessageLookupByLibrary.simpleMessage("Claro"), "lightTheme": MessageLookupByLibrary.simpleMessage("Claro"), "link": MessageLookupByLibrary.simpleMessage("Vincular"), "linkCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Link copiado para a área de transferência"), "linkDeviceLimit": - MessageLookupByLibrary.simpleMessage("Limite do dispositivo"), + MessageLookupByLibrary.simpleMessage("Limite de dispositivo"), "linkEmail": MessageLookupByLibrary.simpleMessage("Vincular e-mail"), "linkEmailToContactBannerCaption": MessageLookupByLibrary.simpleMessage("para compartilhar rápido"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Ativado"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expirado"), - "linkExpiresOn": m45, - "linkExpiry": MessageLookupByLibrary.simpleMessage("Expiração do link"), + "linkExpiresOn": m46, + "linkExpiry": MessageLookupByLibrary.simpleMessage("Link expirado"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("O link expirou"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Nunca"), "linkPerson": MessageLookupByLibrary.simpleMessage("Vincular pessoa"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "para melhor experiência de compartilhamento"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, - "livePhotos": MessageLookupByLibrary.simpleMessage("Fotos animadas"), + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, + "livePhotos": + MessageLookupByLibrary.simpleMessage("Fotos Em Tempo Real"), "loadMessage1": MessageLookupByLibrary.simpleMessage( - "Você pode compartilhar sua assinatura com seus familiares"), - "loadMessage2": MessageLookupByLibrary.simpleMessage( - "Nós preservamos mais de 30 milhões de memórias até então"), + "Pode partilhar a sua subscrição com a sua família"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Mantemos 3 cópias dos seus dados, uma em um abrigo subterrâneo"), "loadMessage4": MessageLookupByLibrary.simpleMessage( "Todos os nossos aplicativos são de código aberto"), "loadMessage5": MessageLookupByLibrary.simpleMessage( "Nosso código-fonte e criptografia foram auditadas externamente"), - "loadMessage6": MessageLookupByLibrary.simpleMessage( - "Você pode compartilhar links para seus álbuns com seus entes queridos"), + "loadMessage6": + MessageLookupByLibrary.simpleMessage("Deixar o álbum partilhado?"), "loadMessage7": MessageLookupByLibrary.simpleMessage( - "Nossos aplicativos móveis são executados em segundo plano para criptografar e copiar com segurança quaisquer fotos novas que você acessar"), + "Nossos aplicativos móveis são executados em segundo plano para criptografar e fazer backup de quaisquer novas fotos que você clique"), "loadMessage8": MessageLookupByLibrary.simpleMessage( - "web.ente.io tem um enviador mais rápido"), + "web.ente.io tem um envio mais rápido"), "loadMessage9": MessageLookupByLibrary.simpleMessage( "Nós usamos Xchacha20Poly1305 para criptografar seus dados com segurança"), "loadingExifData": @@ -1232,95 +1174,95 @@ class MessageLookup extends MessageLookupByLibrary { "loadingGallery": MessageLookupByLibrary.simpleMessage("Carregando galeria..."), "loadingMessage": - MessageLookupByLibrary.simpleMessage("Carregando suas fotos..."), + MessageLookupByLibrary.simpleMessage("Carregar as suas fotos..."), "loadingModel": - MessageLookupByLibrary.simpleMessage("Baixando modelos..."), + MessageLookupByLibrary.simpleMessage("Transferindo modelos..."), "loadingYourPhotos": - MessageLookupByLibrary.simpleMessage("Carregando suas fotos..."), + MessageLookupByLibrary.simpleMessage("Carregar as suas fotos..."), "localGallery": MessageLookupByLibrary.simpleMessage("Galeria local"), "localIndexing": MessageLookupByLibrary.simpleMessage("Indexação local"), "localSyncErrorMessage": MessageLookupByLibrary.simpleMessage( - "Ocorreu um erro devido à sincronização de localização das fotos estar levando mais tempo que o esperado. Entre em contato conosco."), + "Parece que algo correu mal, uma vez que a sincronização de fotografias locais está a demorar mais tempo do que o esperado. Contacte a nossa equipa de apoio"), "location": MessageLookupByLibrary.simpleMessage("Localização"), "locationName": MessageLookupByLibrary.simpleMessage("Nome da localização"), "locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage( - "Uma etiqueta de localização agrupa todas as fotos fotografadas em algum raio de uma foto"), + "Uma etiqueta de localização agrupa todas as fotos que foram tiradas num determinado raio de uma fotografia"), "locations": MessageLookupByLibrary.simpleMessage("Localizações"), "lockButtonLabel": MessageLookupByLibrary.simpleMessage("Bloquear"), - "lockscreen": MessageLookupByLibrary.simpleMessage("Tela de bloqueio"), - "logInLabel": MessageLookupByLibrary.simpleMessage("Entrar"), - "loggingOut": MessageLookupByLibrary.simpleMessage("Desconectando..."), + "lockscreen": MessageLookupByLibrary.simpleMessage("Ecrã de bloqueio"), + "logInLabel": MessageLookupByLibrary.simpleMessage("Iniciar sessão"), + "loggingOut": + MessageLookupByLibrary.simpleMessage("Terminar a sessão..."), "loginSessionExpired": MessageLookupByLibrary.simpleMessage("Sessão expirada"), "loginSessionExpiredDetails": MessageLookupByLibrary.simpleMessage( - "Sua sessão expirou. Registre-se novamente."), + "A sua sessão expirou. Por favor, inicie sessão novamente."), "loginTerms": MessageLookupByLibrary.simpleMessage( - "Ao clicar em entrar, eu concordo com os termos de serviço e a política de privacidade"), + "Ao clicar em iniciar sessão, eu concordo com os termos de serviço e política de privacidade"), "loginWithTOTP": - MessageLookupByLibrary.simpleMessage("Registrar com TOTP"), - "logout": MessageLookupByLibrary.simpleMessage("Encerrar sessão"), + MessageLookupByLibrary.simpleMessage("Iniciar sessão com TOTP"), + "logout": MessageLookupByLibrary.simpleMessage("Terminar sessão"), "logsDialogBody": MessageLookupByLibrary.simpleMessage( - "Isso enviará através dos registros para ajudar-nos a resolver seu problema. Saiba que, nome de arquivos serão incluídos para ajudar a buscar problemas com arquivos específicos."), + "Isto enviará os registos para nos ajudar a resolver o problema. Tenha em atenção que os nomes dos ficheiros serão incluídos para ajudar a localizar problemas com ficheiros específicos."), "longPressAnEmailToVerifyEndToEndEncryption": MessageLookupByLibrary.simpleMessage( - "Pressione um e-mail para verificar a criptografia ponta a ponta."), + "Pressione e segure um e-mail para verificar a criptografia de ponta a ponta."), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( - "Mantenha pressionado em um item para visualizá-lo em tela cheia"), + "Pressione e segure em um item para ver em tela cheia"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": - MessageLookupByLibrary.simpleMessage("Repetir vídeo desativado"), + MessageLookupByLibrary.simpleMessage("Repetir vídeo desligado"), "loopVideoOn": - MessageLookupByLibrary.simpleMessage("Repetir vídeo ativado"), + MessageLookupByLibrary.simpleMessage("Repetir vídeo ligado"), "lostDevice": - MessageLookupByLibrary.simpleMessage("Perdeu o dispositivo?"), + MessageLookupByLibrary.simpleMessage("Perdeu o seu dispositívo?"), "machineLearning": MessageLookupByLibrary.simpleMessage("Aprendizagem automática"), - "magicSearch": MessageLookupByLibrary.simpleMessage("Busca mágica"), + "magicSearch": MessageLookupByLibrary.simpleMessage("Pesquisa mágica"), "magicSearchHint": MessageLookupByLibrary.simpleMessage( - "A busca mágica permite buscar fotos pelo conteúdo, p. e.x. \'flor\', \'carro vermelho\', \'identidade\'"), - "manage": MessageLookupByLibrary.simpleMessage("Gerenciar"), - "manageDeviceStorage": MessageLookupByLibrary.simpleMessage( - "Gerenciar cache do dispositivo"), + "A pesquisa mágica permite pesquisar fotos por seu conteúdo, por exemplo, \'flor\', \'carro vermelho\', \'documentos de identidade\'"), + "manage": MessageLookupByLibrary.simpleMessage("Gerir"), "manageDeviceStorageDesc": MessageLookupByLibrary.simpleMessage( "Reveja e limpe o armazenamento de cache local."), - "manageFamily": - MessageLookupByLibrary.simpleMessage("Gerenciar família"), - "manageLink": MessageLookupByLibrary.simpleMessage("Gerenciar link"), - "manageParticipants": MessageLookupByLibrary.simpleMessage("Gerenciar"), + "manageFamily": MessageLookupByLibrary.simpleMessage("Gerir família"), + "manageLink": MessageLookupByLibrary.simpleMessage("Gerir link"), + "manageParticipants": MessageLookupByLibrary.simpleMessage("Gerir"), "manageSubscription": - MessageLookupByLibrary.simpleMessage("Gerenciar assinatura"), + MessageLookupByLibrary.simpleMessage("Gerir subscrição"), "manualPairDesc": MessageLookupByLibrary.simpleMessage( - "Parear com PIN funciona com qualquer tela que queira visualizar seu álbum."), + "Emparelhar com PIN funciona com qualquer ecrã onde pretenda ver o seu álbum."), "map": MessageLookupByLibrary.simpleMessage("Mapa"), "maps": MessageLookupByLibrary.simpleMessage("Mapas"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Eu"), - "memoryCount": m48, "merchandise": MessageLookupByLibrary.simpleMessage("Produtos"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Juntar com o existente"), - "mergedPhotos": MessageLookupByLibrary.simpleMessage("Fotos mescladas"), + "mergedPhotos": + MessageLookupByLibrary.simpleMessage("Fotos combinadas"), "mlConsent": MessageLookupByLibrary.simpleMessage( "Ativar aprendizagem automática"), "mlConsentConfirmation": MessageLookupByLibrary.simpleMessage( "Eu entendo, e desejo ativar a aprendizagem automática"), "mlConsentDescription": MessageLookupByLibrary.simpleMessage( - "Se você ativar a aprendizagem automática, o Ente irá extrair informações como geometria de rosto dos arquivos, incluindo os compartilhados com você.\n\nIsso acontecerá no seu dispositivo, qualquer informação biométrica gerada será criptografada ponta a ponta."), + "Se ativar a aprendizagem automática, o Ente extrairá informações como a geometria do rosto de ficheiros, incluindo os partilhados consigo.\n\n\nIsto acontecerá no seu dispositivo e todas as informações biométricas geradas serão encriptadas de ponta a ponta."), "mlConsentPrivacy": MessageLookupByLibrary.simpleMessage( - "Clique aqui para mais detalhes sobre este recurso na política de privacidade"), + "Por favor, clique aqui para mais detalhes sobre este recurso na nossa política de privacidade"), "mlConsentTitle": MessageLookupByLibrary.simpleMessage( "Ativar aprendizagem automática?"), "mlIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Note que a aprendizagem automática resultará em uso de bateria e largura de banda maior até que todos os itens forem indexados. Considere-se usar o aplicativo para notebook para uma indexação mais rápida, todos os resultados serão sincronizados automaticamente."), + "Tenha em atenção que a aprendizagem automática resultará numa maior utilização da largura de banda e da bateria até que todos os itens sejam indexados. Considere utilizar a aplicação de ambiente de trabalho para uma indexação mais rápida, todos os resultados serão sincronizados automaticamente."), "mobileWebDesktop": - MessageLookupByLibrary.simpleMessage("Celular, Web, Computador"), - "moderateStrength": MessageLookupByLibrary.simpleMessage("Moderado"), + MessageLookupByLibrary.simpleMessage("Mobile, Web, Desktop"), + "moderateStrength": MessageLookupByLibrary.simpleMessage("Moderada"), "modifyYourQueryOrTrySearchingFor": MessageLookupByLibrary.simpleMessage( - "Altere o termo de busca ou tente consultar"), + "Modifique a sua consulta ou tente pesquisar por"), "moments": MessageLookupByLibrary.simpleMessage("Momentos"), "month": MessageLookupByLibrary.simpleMessage("mês"), "monthly": MessageLookupByLibrary.simpleMessage("Mensal"), @@ -1329,42 +1271,41 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Mais recente"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Mais relevante"), "mountains": MessageLookupByLibrary.simpleMessage("Sob as montanhas"), - "moveItem": m49, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Mover fotos selecionadas para uma data"), - "moveToAlbum": - MessageLookupByLibrary.simpleMessage("Mover para o álbum"), + "moveToAlbum": MessageLookupByLibrary.simpleMessage("Mover para álbum"), "moveToHiddenAlbum": - MessageLookupByLibrary.simpleMessage("Mover ao álbum oculto"), - "movedSuccessfullyTo": m50, + MessageLookupByLibrary.simpleMessage("Mover para álbum oculto"), + "movedSuccessfullyTo": m51, "movedToTrash": - MessageLookupByLibrary.simpleMessage("Movido para a lixeira"), + MessageLookupByLibrary.simpleMessage("Mover para o lixo"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( - "Movendo arquivos para o álbum..."), + "Mover arquivos para o álbum..."), "name": MessageLookupByLibrary.simpleMessage("Nome"), - "nameTheAlbum": MessageLookupByLibrary.simpleMessage("Nomear álbum"), + "nameTheAlbum": MessageLookupByLibrary.simpleMessage("Nomear o álbum"), "networkConnectionRefusedErr": MessageLookupByLibrary.simpleMessage( - "Não foi possível conectar ao Ente, tente novamente mais tarde. Se o erro persistir, entre em contato com o suporte."), + "Não foi possível conectar ao Ente, tente novamente após algum tempo. Se o erro persistir, entre em contato com o suporte."), "networkHostLookUpErr": MessageLookupByLibrary.simpleMessage( - "Não foi possível conectar-se ao Ente, verifique suas configurações de rede e entre em contato com o suporte se o erro persistir."), + "Não foi possível estabelecer ligação ao Ente. Verifique as definições de rede e contacte o serviço de apoio se o erro persistir."), "never": MessageLookupByLibrary.simpleMessage("Nunca"), "newAlbum": MessageLookupByLibrary.simpleMessage("Novo álbum"), "newLocation": MessageLookupByLibrary.simpleMessage("Nova localização"), "newPerson": MessageLookupByLibrary.simpleMessage("Nova pessoa"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newRange": MessageLookupByLibrary.simpleMessage("Novo intervalo"), "newToEnte": MessageLookupByLibrary.simpleMessage("Novo no Ente"), - "newest": MessageLookupByLibrary.simpleMessage("Mais recente"), - "next": MessageLookupByLibrary.simpleMessage("Próximo"), + "newest": MessageLookupByLibrary.simpleMessage("Recentes"), + "next": MessageLookupByLibrary.simpleMessage("Seguinte"), "no": MessageLookupByLibrary.simpleMessage("Não"), "noAlbumsSharedByYouYet": MessageLookupByLibrary.simpleMessage( - "Nenhum álbum compartilhado por você ainda"), + "Ainda não há álbuns partilhados por si"), "noDeviceFound": MessageLookupByLibrary.simpleMessage( "Nenhum dispositivo encontrado"), "noDeviceLimit": MessageLookupByLibrary.simpleMessage("Nenhum"), "noDeviceThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( - "Você não tem arquivos neste dispositivo que possam ser excluídos"), + "Você não tem arquivos neste dispositivo que possam ser apagados"), "noDuplicates": - MessageLookupByLibrary.simpleMessage("✨ Sem duplicatas"), + MessageLookupByLibrary.simpleMessage("✨ Sem duplicados"), "noEnteAccountExclamation": MessageLookupByLibrary.simpleMessage("Nenhuma conta Ente!"), "noExifData": MessageLookupByLibrary.simpleMessage("Sem dados EXIF"), @@ -1375,42 +1316,45 @@ class MessageLookup extends MessageLookupByLibrary { "noImagesWithLocation": MessageLookupByLibrary.simpleMessage( "Nenhuma imagem com localização"), "noInternetConnection": - MessageLookupByLibrary.simpleMessage("Sem conexão à internet"), + MessageLookupByLibrary.simpleMessage("Sem ligação à internet"), "noPhotosAreBeingBackedUpRightNow": MessageLookupByLibrary.simpleMessage( - "No momento não há fotos sendo copiadas com segurança"), + "No momento não há backup de fotos sendo feito"), "noPhotosFoundHere": MessageLookupByLibrary.simpleMessage( "Nenhuma foto encontrada aqui"), "noQuickLinksSelected": MessageLookupByLibrary.simpleMessage( "Nenhum link rápido selecionado"), - "noRecoveryKey": - MessageLookupByLibrary.simpleMessage("Sem chave de recuperação?"), + "noRecoveryKey": MessageLookupByLibrary.simpleMessage( + "Não tem chave de recuperação?"), "noRecoveryKeyNoDecryption": MessageLookupByLibrary.simpleMessage( - "Devido à natureza do nosso protocolo de criptografia de ponta a ponta, seus dados não podem ser descriptografados sem sua senha ou chave de recuperação"), + "Devido à natureza do nosso protocolo de criptografia de ponta a ponta, os seus dados não podem ser descriptografados sem a sua palavra-passe ou a sua chave de recuperação"), "noResults": MessageLookupByLibrary.simpleMessage("Nenhum resultado"), - "noResultsFound": - MessageLookupByLibrary.simpleMessage("Nenhum resultado encontrado"), - "noSuggestionsForPerson": m51, + "noResultsFound": MessageLookupByLibrary.simpleMessage( + "Não foram encontrados resultados"), + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( - "Nenhum bloqueio do sistema encontrado"), - "notPersonLabel": m52, + "Nenhum bloqueio de sistema encontrado"), + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("Não é esta pessoa?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( - "Nada compartilhado com você ainda"), + "Ainda nada partilhado consigo"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage("Nada para ver aqui! 👀"), "notifications": MessageLookupByLibrary.simpleMessage("Notificações"), - "ok": MessageLookupByLibrary.simpleMessage("OK"), + "ok": MessageLookupByLibrary.simpleMessage("Ok"), "onDevice": MessageLookupByLibrary.simpleMessage("No dispositivo"), "onEnte": MessageLookupByLibrary.simpleMessage( - "No ente"), + "Em ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("Na estrada de novo"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Apenas eles"), - "oops": MessageLookupByLibrary.simpleMessage("Ops"), + "oops": MessageLookupByLibrary.simpleMessage("Oops"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( - "Opa! Não foi possível salvar as edições"), + "Oops, não foi possível guardar as edições"), "oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage("Ops, algo deu errado"), "openAlbumInBrowser": @@ -1418,74 +1362,75 @@ class MessageLookup extends MessageLookupByLibrary { "openAlbumInBrowserTitle": MessageLookupByLibrary.simpleMessage( "Use o aplicativo da web para adicionar fotos a este álbum"), "openFile": MessageLookupByLibrary.simpleMessage("Abrir arquivo"), - "openSettings": MessageLookupByLibrary.simpleMessage("Abrir opções"), - "openTheItem": - MessageLookupByLibrary.simpleMessage("• Abra a foto ou vídeo"), + "openSettings": + MessageLookupByLibrary.simpleMessage("Abrir Definições"), + "openTheItem": MessageLookupByLibrary.simpleMessage("• Abra o item"), "openstreetmapContributors": MessageLookupByLibrary.simpleMessage( "Contribuidores do OpenStreetMap"), "optionalAsShortAsYouLike": MessageLookupByLibrary.simpleMessage( - "Opcional, tão curto como quiser..."), - "orMergeWithExistingPerson": - MessageLookupByLibrary.simpleMessage("Ou mesclar com existente"), + "Opcional, o mais breve que quiser..."), + "orMergeWithExistingPerson": MessageLookupByLibrary.simpleMessage( + "Ou combinar com já existente"), "orPickAnExistingOne": - MessageLookupByLibrary.simpleMessage("Ou escolha um existente"), + MessageLookupByLibrary.simpleMessage("Ou escolha um já existente"), "orPickFromYourContacts": MessageLookupByLibrary.simpleMessage( "ou escolher dos seus contatos"), - "pair": MessageLookupByLibrary.simpleMessage("Parear"), - "pairWithPin": MessageLookupByLibrary.simpleMessage("Parear com PIN"), + "pair": MessageLookupByLibrary.simpleMessage("Emparelhar"), + "pairWithPin": + MessageLookupByLibrary.simpleMessage("Emparelhar com PIN"), "pairingComplete": - MessageLookupByLibrary.simpleMessage("Pareamento concluído"), + MessageLookupByLibrary.simpleMessage("Emparelhamento concluído"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), - "partyWithThem": m54, - "passKeyPendingVerification": - MessageLookupByLibrary.simpleMessage("Verificação pendente"), + "partyWithThem": m55, + "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage( + "A verificação ainda está pendente"), "passkey": MessageLookupByLibrary.simpleMessage("Chave de acesso"), "passkeyAuthTitle": MessageLookupByLibrary.simpleMessage( - "Verificação de chave de acesso"), - "password": MessageLookupByLibrary.simpleMessage("Senha"), - "passwordChangedSuccessfully": - MessageLookupByLibrary.simpleMessage("Senha alterada com sucesso"), + "Verificação da chave de acesso"), + "password": MessageLookupByLibrary.simpleMessage("Palavra-passe"), + "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( + "Palavra-passe alterada com sucesso"), "passwordLock": - MessageLookupByLibrary.simpleMessage("Bloqueio por senha"), - "passwordStrength": m55, + MessageLookupByLibrary.simpleMessage("Bloqueio da palavra-passe"), + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( - "A força da senha é calculada considerando o comprimento dos dígitos, carácteres usados, e se ou não a senha aparece nas 10.000 senhas usadas."), + "A força da palavra-passe é calculada tendo em conta o comprimento da palavra-passe, os caracteres utilizados e se a palavra-passe aparece ou não nas 10.000 palavras-passe mais utilizadas"), "passwordWarning": MessageLookupByLibrary.simpleMessage( - "Nós não armazenamos esta senha, se você esquecer, nós não poderemos descriptografar seus dados"), + "Não armazenamos esta palavra-passe, se você a esquecer, não podemos desencriptar os seus dados"), "paymentDetails": MessageLookupByLibrary.simpleMessage("Detalhes de pagamento"), "paymentFailed": MessageLookupByLibrary.simpleMessage("O pagamento falhou"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( - "Infelizmente o pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!"), - "paymentFailedTalkToProvider": m56, + "Infelizmente o seu pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!"), + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"), "pendingSync": MessageLookupByLibrary.simpleMessage("Sincronização pendente"), "people": MessageLookupByLibrary.simpleMessage("Pessoas"), - "peopleUsingYourCode": - MessageLookupByLibrary.simpleMessage("Pessoas que usam seu código"), + "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage( + "Pessoas que utilizam seu código"), "permDeleteWarning": MessageLookupByLibrary.simpleMessage( - "Todos os itens na lixeira serão excluídos permanentemente\n\nEsta ação não pode ser desfeita"), + "Todos os itens no lixo serão permanentemente eliminados\n\n\nEsta ação não pode ser anulada"), "permanentlyDelete": - MessageLookupByLibrary.simpleMessage("Excluir permanentemente"), + MessageLookupByLibrary.simpleMessage("Eliminar permanentemente"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( - "Excluir permanentemente do dispositivo?"), - "personIsAge": m57, + "Apagar permanentemente do dispositivo?"), + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Nome da pessoa"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Companhia de pelos"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Descrições das fotos"), "photoGridSize": - MessageLookupByLibrary.simpleMessage("Tamanho da grade de fotos"), + MessageLookupByLibrary.simpleMessage("Tamanho da grelha de fotos"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("foto"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Fotos"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( - "Suas fotos adicionadas serão removidas do álbum"), - "photosCount": m60, + "As fotos adicionadas por si serão removidas do álbum"), + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "As fotos mantêm a diferença de tempo relativo"), @@ -1497,43 +1442,45 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Reproduzir álbum na TV"), "playOriginal": MessageLookupByLibrary.simpleMessage("Reproduzir original"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Reproduzir transmissão"), "playstoreSubscription": - MessageLookupByLibrary.simpleMessage("Assinatura da PlayStore"), + MessageLookupByLibrary.simpleMessage("Subscrição da PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": MessageLookupByLibrary.simpleMessage( - "Verifique sua conexão com a internet e tente novamente."), + "Por favor, verifique a sua ligação à Internet e tente novamente."), "pleaseContactSupportAndWeWillBeHappyToHelp": MessageLookupByLibrary.simpleMessage( - "Entre em contato com support@ente.io e nós ficaremos felizes em ajudar!"), + "Por favor, entre em contato com support@ente.io e nós ficaremos felizes em ajudar!"), "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Por favor, contate o suporte se o problema persistir"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Por favor, conceda as permissões"), - "pleaseLoginAgain": - MessageLookupByLibrary.simpleMessage("Registre-se novamente"), + "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage( + "Por favor, inicie sessão novamente"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Selecione links rápidos para remover"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": - MessageLookupByLibrary.simpleMessage("Tente novamente"), + MessageLookupByLibrary.simpleMessage("Por favor, tente novamente"), "pleaseVerifyTheCodeYouHaveEntered": - MessageLookupByLibrary.simpleMessage("Verifique o código inserido"), - "pleaseWait": MessageLookupByLibrary.simpleMessage("Aguarde..."), - "pleaseWaitDeletingAlbum": - MessageLookupByLibrary.simpleMessage("Aguarde, excluindo álbum"), + MessageLookupByLibrary.simpleMessage( + "Por favor, verifique se o código que você inseriu"), + "pleaseWait": + MessageLookupByLibrary.simpleMessage("Por favor, aguarde ..."), + "pleaseWaitDeletingAlbum": MessageLookupByLibrary.simpleMessage( + "Por favor aguarde, apagar o álbum"), "pleaseWaitForSometimeBeforeRetrying": MessageLookupByLibrary.simpleMessage( - "Por favor, aguarde mais algum tempo antes de tentar novamente"), + "Por favor, aguarde algum tempo antes de tentar novamente"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Aguarde um pouco, isso talvez leve um tempo."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": - MessageLookupByLibrary.simpleMessage("Preparando registros..."), + MessageLookupByLibrary.simpleMessage("Preparando logs..."), "preserveMore": MessageLookupByLibrary.simpleMessage("Preservar mais"), "pressAndHoldToPlayVideo": MessageLookupByLibrary.simpleMessage( "Pressione e segure para reproduzir o vídeo"), @@ -1542,31 +1489,29 @@ class MessageLookup extends MessageLookupByLibrary { "previous": MessageLookupByLibrary.simpleMessage("Anterior"), "privacy": MessageLookupByLibrary.simpleMessage("Privacidade"), "privacyPolicyTitle": - MessageLookupByLibrary.simpleMessage("Política de Privacidade"), + MessageLookupByLibrary.simpleMessage("Política de privacidade"), "privateBackups": - MessageLookupByLibrary.simpleMessage("Cópias privadas"), + MessageLookupByLibrary.simpleMessage("Backups privados"), "privateSharing": - MessageLookupByLibrary.simpleMessage("Compartilhamento privado"), + MessageLookupByLibrary.simpleMessage("Partilha privada"), "proceed": MessageLookupByLibrary.simpleMessage("Continuar"), - "processed": MessageLookupByLibrary.simpleMessage("Processado"), "processing": MessageLookupByLibrary.simpleMessage("Processando"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Processando vídeos"), "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Link público criado"), "publicLinkEnabled": - MessageLookupByLibrary.simpleMessage("Link público ativo"), + MessageLookupByLibrary.simpleMessage("Link público ativado"), "queued": MessageLookupByLibrary.simpleMessage("Na fila"), "quickLinks": MessageLookupByLibrary.simpleMessage("Links rápidos"), "radius": MessageLookupByLibrary.simpleMessage("Raio"), "raiseTicket": MessageLookupByLibrary.simpleMessage("Abrir ticket"), - "rateTheApp": - MessageLookupByLibrary.simpleMessage("Avalie o aplicativo"), - "rateUs": MessageLookupByLibrary.simpleMessage("Avaliar"), - "rateUsOnStore": m66, + "rateTheApp": MessageLookupByLibrary.simpleMessage("Avaliar aplicação"), + "rateUs": MessageLookupByLibrary.simpleMessage("Avalie-nos"), + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("Reatribuir \"Eu\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Reatribuindo..."), "recover": MessageLookupByLibrary.simpleMessage("Recuperar"), @@ -1577,71 +1522,72 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recuperar conta"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("A recuperação iniciou"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Chave de recuperação"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Chave de recuperação copiada para a área de transferência"), "recoveryKeyOnForgotPassword": MessageLookupByLibrary.simpleMessage( - "Caso você esqueça sua senha, a única maneira de recuperar seus dados é com esta chave."), + "Se esquecer sua palavra-passe, a única maneira de recuperar os seus dados é com esta chave."), "recoveryKeySaveDescription": MessageLookupByLibrary.simpleMessage( - "Não armazenamos esta chave, salve esta chave de 24 palavras em um lugar seguro."), + "Não armazenamos essa chave, por favor, guarde esta chave de 24 palavras num lugar seguro."), "recoveryKeySuccessBody": MessageLookupByLibrary.simpleMessage( - "Ótimo! Sua chave de recuperação é válida. Obrigada por verificar.\n\nLembre-se de manter sua chave de recuperação copiada com segurança."), + "Ótimo! A sua chave de recuperação é válida. Obrigado por verificar.\n\nLembre-se de manter cópia de segurança da sua chave de recuperação."), "recoveryKeyVerified": MessageLookupByLibrary.simpleMessage( "Chave de recuperação verificada"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( - "Sua chave de recuperação é a única maneira de recuperar suas fotos se você esqueceu sua senha. Você pode encontrar sua chave de recuperação em Opções > Conta.\n\nInsira sua chave de recuperação aqui para verificar se você a salvou corretamente."), - "recoveryReady": m69, + "A sua chave de recuperação é a única forma de recuperar as suas fotografias se se esquecer da sua palavra-passe. Pode encontrar a sua chave de recuperação em Definições > Conta.\n\n\nIntroduza aqui a sua chave de recuperação para verificar se a guardou corretamente."), + "recoveryReady": m70, "recoverySuccessful": - MessageLookupByLibrary.simpleMessage("Recuperação com sucesso!"), + MessageLookupByLibrary.simpleMessage("Recuperação bem sucedida!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Um contato confiável está tentando acessar sua conta"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( - "O dispositivo atual não é poderoso o suficiente para verificar sua senha, no entanto, nós podemos regenerar numa maneira que funciona em todos os dispositivos.\n\nEntre usando a chave de recuperação e regenere sua senha (você pode usar a mesma novamente se desejar)."), + "O dispositivo atual não é suficientemente poderoso para verificar a palavra-passe, mas podemos regenerar novamente de uma maneira que funcione no seu dispositivo.\n\nPor favor, iniciar sessão utilizando código de recuperação e gerar novamente a sua palavra-passe (pode utilizar a mesma se quiser)."), "recreatePasswordTitle": - MessageLookupByLibrary.simpleMessage("Redefinir senha"), + MessageLookupByLibrary.simpleMessage("Recriar palavra-passe"), "reddit": MessageLookupByLibrary.simpleMessage("Reddit"), - "reenterPassword": - MessageLookupByLibrary.simpleMessage("Reinserir senha"), - "reenterPin": MessageLookupByLibrary.simpleMessage("Reinserir PIN"), + "reenterPassword": MessageLookupByLibrary.simpleMessage( + "Insira novamente a palavra-passe"), + "reenterPin": + MessageLookupByLibrary.simpleMessage("Inserir PIN novamente"), "referFriendsAnd2xYourPlan": MessageLookupByLibrary.simpleMessage( - "Recomende seus amigos e duplique seu plano"), + "Recomende amigos e duplique o seu plano"), "referralStep1": MessageLookupByLibrary.simpleMessage( "1. Envie este código aos seus amigos"), "referralStep2": MessageLookupByLibrary.simpleMessage( - "2. Eles então se inscrevem num plano pago"), - "referralStep3": m71, + "2. Eles se inscrevem em um plano pago"), + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Referências"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( - "As referências estão atualmente pausadas"), + "As referências estão atualmente em pausa"), "rejectRecovery": MessageLookupByLibrary.simpleMessage("Rejeitar recuperação"), "remindToEmptyDeviceTrash": MessageLookupByLibrary.simpleMessage( - "Também vazio \"Excluído recentemente\" de \"Opções\" -> \"Armazenamento\" para reivindicar espaço liberado"), + "Esvazie também a opção “Eliminados recentemente” em “Definições” -> “Armazenamento” para reclamar o espaço libertado"), "remindToEmptyEnteTrash": MessageLookupByLibrary.simpleMessage( - "Também esvazie sua \"Lixeira\" para reivindicar o espaço liberado"), + "Esvazie também o seu “Lixo” para reivindicar o espaço libertado"), "remoteImages": MessageLookupByLibrary.simpleMessage("Imagens remotas"), "remoteThumbnails": MessageLookupByLibrary.simpleMessage("Miniaturas remotas"), "remoteVideos": MessageLookupByLibrary.simpleMessage("Vídeos remotos"), "remove": MessageLookupByLibrary.simpleMessage("Remover"), "removeDuplicates": - MessageLookupByLibrary.simpleMessage("Excluir duplicatas"), + MessageLookupByLibrary.simpleMessage("Remover duplicados"), "removeDuplicatesDesc": MessageLookupByLibrary.simpleMessage( - "Revise e remova arquivos que são duplicatas exatas."), + "Rever e remover ficheiros que sejam duplicados exatos."), "removeFromAlbum": MessageLookupByLibrary.simpleMessage("Remover do álbum"), "removeFromAlbumTitle": - MessageLookupByLibrary.simpleMessage("Remover do álbum?"), + MessageLookupByLibrary.simpleMessage("Remover do álbum"), "removeFromFavorite": - MessageLookupByLibrary.simpleMessage("Desfavoritar"), + MessageLookupByLibrary.simpleMessage("Remover dos favoritos"), "removeInvite": MessageLookupByLibrary.simpleMessage("Remover convite"), "removeLink": MessageLookupByLibrary.simpleMessage("Remover link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Remover participante"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Remover etiqueta da pessoa"), "removePublicLink": @@ -1660,15 +1606,15 @@ class MessageLookup extends MessageLookupByLibrary { "renameAlbum": MessageLookupByLibrary.simpleMessage("Renomear álbum"), "renameFile": MessageLookupByLibrary.simpleMessage("Renomear arquivo"), "renewSubscription": - MessageLookupByLibrary.simpleMessage("Renovar assinatura"), - "renewsOn": m73, - "reportABug": MessageLookupByLibrary.simpleMessage("Informar um erro"), - "reportBug": MessageLookupByLibrary.simpleMessage("Informar erro"), + MessageLookupByLibrary.simpleMessage("Renovar subscrição"), + "renewsOn": m74, + "reportABug": MessageLookupByLibrary.simpleMessage("Reporte um bug"), + "reportBug": MessageLookupByLibrary.simpleMessage("Reportar bug"), "resendEmail": MessageLookupByLibrary.simpleMessage("Reenviar e-mail"), - "resetIgnoredFiles": MessageLookupByLibrary.simpleMessage( - "Redefinir arquivos ignorados"), + "resetIgnoredFiles": + MessageLookupByLibrary.simpleMessage("Repor ficheiros ignorados"), "resetPasswordTitle": - MessageLookupByLibrary.simpleMessage("Redefinir senha"), + MessageLookupByLibrary.simpleMessage("Redefinir palavra-passe"), "resetPerson": MessageLookupByLibrary.simpleMessage("Remover"), "resetToDefault": MessageLookupByLibrary.simpleMessage("Redefinir para o padrão"), @@ -1676,83 +1622,79 @@ class MessageLookup extends MessageLookupByLibrary { "restoreToAlbum": MessageLookupByLibrary.simpleMessage("Restaurar para álbum"), "restoringFiles": - MessageLookupByLibrary.simpleMessage("Restaurando arquivos..."), + MessageLookupByLibrary.simpleMessage("Restaurar arquivos..."), "resumableUploads": - MessageLookupByLibrary.simpleMessage("Envios retomáveis"), + MessageLookupByLibrary.simpleMessage("Uploads reenviados"), "retry": MessageLookupByLibrary.simpleMessage("Tentar novamente"), - "review": MessageLookupByLibrary.simpleMessage("Revisar"), + "review": MessageLookupByLibrary.simpleMessage("Rever"), "reviewDeduplicateItems": MessageLookupByLibrary.simpleMessage( - "Reveja e exclua os itens que você acredita serem duplicados."), + "Reveja e elimine os itens que considera serem duplicados."), "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Revisar sugestões"), "right": MessageLookupByLibrary.simpleMessage("Direita"), - "roadtripWithThem": m74, - "rotate": MessageLookupByLibrary.simpleMessage("Girar"), + "roadtripWithThem": m75, + "rotate": MessageLookupByLibrary.simpleMessage("Rodar"), "rotateLeft": - MessageLookupByLibrary.simpleMessage("Girar para a esquerda"), + MessageLookupByLibrary.simpleMessage("Rodar para a esquerda"), "rotateRight": - MessageLookupByLibrary.simpleMessage("Girar para a direita"), + MessageLookupByLibrary.simpleMessage("Rodar para a direita"), "safelyStored": MessageLookupByLibrary.simpleMessage("Armazenado com segurança"), - "save": MessageLookupByLibrary.simpleMessage("Salvar"), + "save": MessageLookupByLibrary.simpleMessage("Guardar"), "saveChangesBeforeLeavingQuestion": MessageLookupByLibrary.simpleMessage( "Salvar mudanças antes de sair?"), - "saveCollage": MessageLookupByLibrary.simpleMessage("Salvar colagem"), - "saveCopy": MessageLookupByLibrary.simpleMessage("Salvar cópia"), - "saveKey": MessageLookupByLibrary.simpleMessage("Salvar chave"), - "savePerson": MessageLookupByLibrary.simpleMessage("Salvar pessoa"), + "saveCollage": MessageLookupByLibrary.simpleMessage("Guardar colagem"), + "saveCopy": MessageLookupByLibrary.simpleMessage("Guardar cópia"), + "saveKey": MessageLookupByLibrary.simpleMessage("Guardar chave"), + "savePerson": MessageLookupByLibrary.simpleMessage("Guardar pessoa"), "saveYourRecoveryKeyIfYouHaventAlready": MessageLookupByLibrary.simpleMessage( - "Salve sua chave de recuperação, se você ainda não fez"), - "saving": MessageLookupByLibrary.simpleMessage("Salvando..."), + "Guarde a sua chave de recuperação, caso ainda não o tenha feito"), + "saving": MessageLookupByLibrary.simpleMessage("A gravar..."), "savingEdits": - MessageLookupByLibrary.simpleMessage("Salvando edições..."), - "scanCode": MessageLookupByLibrary.simpleMessage("Escanear código"), + MessageLookupByLibrary.simpleMessage("Gravando edições..."), + "scanCode": MessageLookupByLibrary.simpleMessage("Ler código Qr"), "scanThisBarcodeWithnyourAuthenticatorApp": MessageLookupByLibrary.simpleMessage( - "Escaneie este código de barras com\no aplicativo autenticador"), - "search": MessageLookupByLibrary.simpleMessage("Buscar"), + "Leia este código com a sua aplicação dois fatores."), + "search": MessageLookupByLibrary.simpleMessage("Pesquisar"), "searchAlbumsEmptySection": MessageLookupByLibrary.simpleMessage("Álbuns"), "searchByAlbumNameHint": MessageLookupByLibrary.simpleMessage("Nome do álbum"), "searchByExamples": MessageLookupByLibrary.simpleMessage( - "• Nomes de álbuns (ex: \"Câmera\")\n• Tipos de arquivos (ex.: \"Vídeos\", \".gif\")\n• Anos e meses (ex.: \"2022\", \"Janeiro\")\n• Temporadas (ex.: \"Natal\")\n• Tags (ex.: \"#divertido\")"), + "• Nomes de álbuns (ex: \"Câmera\")\n• Tipos de arquivos (ex.: \"Vídeos\", \".gif\")\n• Anos e meses (e.. \"2022\", \"Janeiro\")\n• Feriados (por exemplo, \"Natal\")\n• Descrições de fotos (por exemplo, \"#divertido\")"), "searchCaptionEmptySection": MessageLookupByLibrary.simpleMessage( - "Adicione marcações como \"#viagem\" nas informações das fotos para encontrá-las aqui com facilidade"), - "searchDatesEmptySection": - MessageLookupByLibrary.simpleMessage("Buscar por data, mês ou ano"), - "searchDiscoverEmptySection": MessageLookupByLibrary.simpleMessage( - "As imagens serão exibidas aqui quando o processamento e sincronização for concluído"), + "Adicione descrições como \"#trip\" nas informações das fotos para encontrá-las aqui rapidamente"), + "searchDatesEmptySection": MessageLookupByLibrary.simpleMessage( + "Pesquisar por data, mês ou ano"), "searchFaceEmptySection": MessageLookupByLibrary.simpleMessage( - "As pessoas apareceram aqui quando a indexação for concluída"), + "As pessoas serão mostradas aqui quando a indexação estiver concluída"), "searchFileTypesAndNamesEmptySection": MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"), - "searchHint1": - MessageLookupByLibrary.simpleMessage("busca rápida no dispositivo"), + "searchHint1": MessageLookupByLibrary.simpleMessage( + "Pesquisa rápida no dispositivo"), "searchHint2": - MessageLookupByLibrary.simpleMessage("Descrições e data das fotos"), + MessageLookupByLibrary.simpleMessage("Datas das fotos, descrições"), "searchHint3": MessageLookupByLibrary.simpleMessage( "Álbuns, nomes de arquivos e tipos"), - "searchHint4": MessageLookupByLibrary.simpleMessage("Localização"), + "searchHint4": MessageLookupByLibrary.simpleMessage("Local"), "searchHint5": MessageLookupByLibrary.simpleMessage( - "Em breve: Busca mágica e rostos ✨"), + "Em breve: Rostos e pesquisa mágica ✨"), "searchLocationEmptySection": MessageLookupByLibrary.simpleMessage( "Fotos de grupo que estão sendo tiradas em algum raio da foto"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( - "Convide pessoas e você verá todas as fotos compartilhadas por elas aqui"), - "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( - "As pessoas serão exibidas aqui quando o processamento e sincronização for concluído"), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "Convide pessoas e verá todas as fotos partilhadas por elas aqui"), + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Segurança"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Ver links de álbum compartilhado no aplicativo"), "selectALocation": - MessageLookupByLibrary.simpleMessage("Selecionar localização"), + MessageLookupByLibrary.simpleMessage("Selecione uma localização"), "selectALocationFirst": MessageLookupByLibrary.simpleMessage( - "Primeiramente selecione uma localização"), + "Selecione uma localização primeiro"), "selectAlbum": MessageLookupByLibrary.simpleMessage("Selecionar álbum"), "selectAll": MessageLookupByLibrary.simpleMessage("Selecionar tudo"), "selectAllShort": MessageLookupByLibrary.simpleMessage("Tudo"), @@ -1760,11 +1702,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Selecionar foto da capa"), "selectDate": MessageLookupByLibrary.simpleMessage("Selecionar data"), "selectFoldersForBackup": MessageLookupByLibrary.simpleMessage( - "Selecionar pastas para copiar com segurança"), + "Selecionar pastas para cópia de segurança"), "selectItemsToAdd": MessageLookupByLibrary.simpleMessage( "Selecionar itens para adicionar"), "selectLanguage": - MessageLookupByLibrary.simpleMessage("Selecionar idioma"), + MessageLookupByLibrary.simpleMessage("Selecionar Idioma"), "selectMailApp": MessageLookupByLibrary.simpleMessage( "Selecionar aplicativo de e-mail"), "selectMorePhotos": @@ -1775,119 +1717,121 @@ class MessageLookup extends MessageLookupByLibrary { "Selecione uma data e hora para todos"), "selectPersonToLink": MessageLookupByLibrary.simpleMessage( "Selecione a pessoa para vincular"), - "selectReason": MessageLookupByLibrary.simpleMessage("Diga o motivo"), + "selectReason": + MessageLookupByLibrary.simpleMessage("Selecionar motivo"), "selectStartOfRange": MessageLookupByLibrary.simpleMessage( "Selecionar início de intervalo"), "selectTime": MessageLookupByLibrary.simpleMessage("Selecionar tempo"), "selectYourFace": MessageLookupByLibrary.simpleMessage("Selecione seu rosto"), "selectYourPlan": - MessageLookupByLibrary.simpleMessage("Selecione seu plano"), + MessageLookupByLibrary.simpleMessage("Selecione o seu plano"), "selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage( "Os arquivos selecionados não estão no Ente"), "selectedFoldersWillBeEncryptedAndBackedUp": MessageLookupByLibrary.simpleMessage( - "As pastas selecionadas serão criptografadas e armazenadas em copiadas com segurança"), + "As pastas selecionadas serão encriptadas e guardadas como cópia de segurança"), "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( - "Os itens selecionados serão excluídos de todos os álbuns e movidos para a lixeira."), + "Os itens selecionados serão eliminados de todos os álbuns e movidos para o lixo."), "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Os itens selecionados serão removidos desta pessoa, entretanto não serão excluídos da sua biblioteca."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, - "selfiesWithThem": m79, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Enviar"), - "sendEmail": MessageLookupByLibrary.simpleMessage("Enviar e-mail"), + "sendEmail": MessageLookupByLibrary.simpleMessage("Enviar email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Enviar convite"), "sendLink": MessageLookupByLibrary.simpleMessage("Enviar link"), "serverEndpoint": - MessageLookupByLibrary.simpleMessage("Ponto final do servidor"), + MessageLookupByLibrary.simpleMessage("Endpoint do servidor"), "sessionExpired": MessageLookupByLibrary.simpleMessage("Sessão expirada"), "sessionIdMismatch": MessageLookupByLibrary.simpleMessage( "Incompatibilidade de ID de sessão"), - "setAPassword": MessageLookupByLibrary.simpleMessage("Definir senha"), + "setAPassword": + MessageLookupByLibrary.simpleMessage("Definir uma palavra-passe"), "setAs": MessageLookupByLibrary.simpleMessage("Definir como"), "setCover": MessageLookupByLibrary.simpleMessage("Definir capa"), "setLabel": MessageLookupByLibrary.simpleMessage("Definir"), "setNewPassword": - MessageLookupByLibrary.simpleMessage("Definir nova senha"), - "setNewPin": MessageLookupByLibrary.simpleMessage("Definir PIN novo"), + MessageLookupByLibrary.simpleMessage("Definir nova palavra-passe"), + "setNewPin": MessageLookupByLibrary.simpleMessage("Definir novo PIN"), "setPasswordTitle": - MessageLookupByLibrary.simpleMessage("Definir senha"), + MessageLookupByLibrary.simpleMessage("Definir palavra-passe"), "setRadius": MessageLookupByLibrary.simpleMessage("Definir raio"), "setupComplete": MessageLookupByLibrary.simpleMessage("Configuração concluída"), - "share": MessageLookupByLibrary.simpleMessage("Compartilhar"), - "shareALink": MessageLookupByLibrary.simpleMessage("Compartilhar link"), + "share": MessageLookupByLibrary.simpleMessage("Partilhar"), + "shareALink": MessageLookupByLibrary.simpleMessage("Partilhar um link"), "shareAlbumHint": MessageLookupByLibrary.simpleMessage( - "Abra um álbum e toque no botão compartilhar no canto superior direito para compartilhar."), + "Abra um álbum e toque no botão de partilha no canto superior direito para partilhar"), "shareAnAlbumNow": - MessageLookupByLibrary.simpleMessage("Compartilhar um álbum agora"), - "shareLink": MessageLookupByLibrary.simpleMessage("Compartilhar link"), - "shareMyVerificationID": m80, + MessageLookupByLibrary.simpleMessage("Partilhar um álbum"), + "shareLink": MessageLookupByLibrary.simpleMessage("Partilhar link"), + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( - "Compartilhar apenas com as pessoas que você quiser"), - "shareTextConfirmOthersVerificationID": m81, + "Partilhar apenas com as pessoas que deseja"), + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( - "Baixe o Ente para que nós possamos compartilhar com facilidade fotos e vídeos de qualidade original\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "Descarregue o Ente para poder partilhar facilmente fotografias e vídeos de qualidade original\n\n\nhttps://ente.io"), + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( - "Compartilhar com usuários não ente"), - "shareWithPeopleSectionTitle": m83, + "Compartilhar com usuários que não usam Ente"), + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( - "Compartilhar seu primeiro álbum"), + "Partilhe o seu primeiro álbum"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( - "Criar álbuns compartilhados e colaborativos com outros usuários Ente, incluindo usuários em planos gratuitos."), + "Criar álbuns compartilhados e colaborativos com outros usuários da Ente, incluindo usuários em planos gratuitos."), "sharedByMe": - MessageLookupByLibrary.simpleMessage("Compartilhada por mim"), + MessageLookupByLibrary.simpleMessage("Partilhado por mim"), "sharedByYou": - MessageLookupByLibrary.simpleMessage("Compartilhado por você"), + MessageLookupByLibrary.simpleMessage("Partilhado por si"), "sharedPhotoNotifications": - MessageLookupByLibrary.simpleMessage("Novas fotos compartilhadas"), + MessageLookupByLibrary.simpleMessage("Novas fotos partilhadas"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( - "Receber notificações quando alguém adicionar uma foto a um álbum compartilhado que você faz parte"), - "sharedWith": m84, + "Receber notificações quando alguém adiciona uma foto a um álbum partilhado do qual faz parte"), + "sharedWith": m85, "sharedWithMe": - MessageLookupByLibrary.simpleMessage("Compartilhado comigo"), + MessageLookupByLibrary.simpleMessage("Partilhado comigo"), "sharedWithYou": - MessageLookupByLibrary.simpleMessage("Compartilhado com você"), - "sharing": MessageLookupByLibrary.simpleMessage("Compartilhando..."), + MessageLookupByLibrary.simpleMessage("Partilhado consigo"), + "sharing": MessageLookupByLibrary.simpleMessage("Partilhar..."), "shiftDatesAndTime": MessageLookupByLibrary.simpleMessage("Alterar as datas e horas"), "showMemories": MessageLookupByLibrary.simpleMessage("Mostrar memórias"), "showPerson": MessageLookupByLibrary.simpleMessage("Mostrar pessoa"), "signOutFromOtherDevices": MessageLookupByLibrary.simpleMessage( - "Sair da conta em outros dispositivos"), + "Terminar sessão noutros dispositivos"), "signOutOtherBody": MessageLookupByLibrary.simpleMessage( - "Se você acha que alguém possa saber da sua senha, você pode forçar desconectar sua conta de outros dispositivos."), - "signOutOtherDevices": - MessageLookupByLibrary.simpleMessage("Sair em outros dispositivos"), + "Se pensa que alguém pode saber a sua palavra-passe, pode forçar todos os outros dispositivos que utilizam a sua conta a terminar a sessão."), + "signOutOtherDevices": MessageLookupByLibrary.simpleMessage( + "Terminar a sessão noutros dispositivos"), "signUpTerms": MessageLookupByLibrary.simpleMessage( - "Eu concordo com os termos de serviço e a política de privacidade"), - "singleFileDeleteFromDevice": m85, + "Eu concordo com os termos de serviço e política de privacidade"), + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( - "Ele será excluído de todos os álbuns."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "Será eliminado de todos os álbuns."), + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Pular"), - "social": MessageLookupByLibrary.simpleMessage("Redes sociais"), + "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": MessageLookupByLibrary.simpleMessage( - "Alguns itens estão em ambos o Ente quanto no seu dispositivo."), + "Alguns itens estão tanto no Ente como no seu dispositivo."), "someOfTheFilesYouAreTryingToDeleteAre": MessageLookupByLibrary.simpleMessage( - "Alguns dos arquivos que você está tentando excluir só estão disponíveis no seu dispositivo e não podem ser recuperados se forem excluídos"), + "Alguns dos ficheiros que está a tentar eliminar só estão disponíveis no seu dispositivo e não podem ser recuperados se forem eliminados"), "someoneSharingAlbumsWithYouShouldSeeTheSameId": MessageLookupByLibrary.simpleMessage( - "Alguém compartilhando álbuns com você deve ver o mesmo ID no dispositivo."), + "Alguém compartilhando álbuns com você deve ver o mesmo ID no seu dispositivo."), "somethingWentWrong": - MessageLookupByLibrary.simpleMessage("Algo deu errado"), + MessageLookupByLibrary.simpleMessage("Ocorreu um erro"), "somethingWentWrongPleaseTryAgain": MessageLookupByLibrary.simpleMessage( - "Algo deu errado. Tente outra vez"), + "Ocorreu um erro. Tente novamente"), "sorry": MessageLookupByLibrary.simpleMessage("Desculpe"), "sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage( "Desculpe, não foi possível adicionar aos favoritos!"), @@ -1896,46 +1840,48 @@ class MessageLookup extends MessageLookupByLibrary { "Desculpe, não foi possível remover dos favoritos!"), "sorryTheCodeYouveEnteredIsIncorrect": MessageLookupByLibrary.simpleMessage( - "O código inserido está incorreto"), + "Desculpe, o código inserido está incorreto"), "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( - "Desculpe, não foi possível gerar chaves seguras neste dispositivo.\n\ninicie sessão com um dispositivo diferente."), + "Desculpe, não foi possível gerar chaves seguras neste dispositivo.\n\npor favor iniciar sessão com um dispositivo diferente."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Ordenar"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Ordenar por"), "sortNewestFirst": - MessageLookupByLibrary.simpleMessage("Recentes primeiro"), + MessageLookupByLibrary.simpleMessage("Mais recentes primeiro"), "sortOldestFirst": - MessageLookupByLibrary.simpleMessage("Antigos primeiro"), + MessageLookupByLibrary.simpleMessage("Mais antigos primeiro"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Sucesso"), - "sportsWithThem": m88, - "spotlightOnThem": m89, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Destacar si mesmo"), "startAccountRecoveryTitle": MessageLookupByLibrary.simpleMessage("Iniciar recuperação"), "startBackup": MessageLookupByLibrary.simpleMessage("Iniciar cópia de segurança"), - "status": MessageLookupByLibrary.simpleMessage("Estado"), - "stopCastingBody": - MessageLookupByLibrary.simpleMessage("Deseja parar a transmissão?"), + "status": MessageLookupByLibrary.simpleMessage("Status"), + "stopCastingBody": MessageLookupByLibrary.simpleMessage( + "Queres parar de fazer transmissão?"), "stopCastingTitle": MessageLookupByLibrary.simpleMessage("Parar transmissão"), "storage": MessageLookupByLibrary.simpleMessage("Armazenamento"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Família"), - "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Você"), - "storageInGB": m90, + "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Tu"), + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Limite de armazenamento excedido"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Detalhes da transmissão"), "strongStrength": MessageLookupByLibrary.simpleMessage("Forte"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, - "subscribe": MessageLookupByLibrary.simpleMessage("Inscrever-se"), + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, + "subscribe": MessageLookupByLibrary.simpleMessage("Subscrever"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( - "Você precisa de uma inscrição paga ativa para ativar o compartilhamento."), - "subscription": MessageLookupByLibrary.simpleMessage("Assinatura"), + "Você precisa de uma assinatura paga ativa para ativar o compartilhamento."), + "subscription": MessageLookupByLibrary.simpleMessage("Subscrição"), "success": MessageLookupByLibrary.simpleMessage("Sucesso"), "successfullyArchived": MessageLookupByLibrary.simpleMessage("Arquivado com sucesso"), @@ -1944,12 +1890,12 @@ class MessageLookup extends MessageLookupByLibrary { "successfullyUnarchived": MessageLookupByLibrary.simpleMessage("Desarquivado com sucesso"), "successfullyUnhid": - MessageLookupByLibrary.simpleMessage("Desocultado com sucesso"), + MessageLookupByLibrary.simpleMessage("Reexibido com sucesso"), "suggestFeatures": - MessageLookupByLibrary.simpleMessage("Sugerir recurso"), + MessageLookupByLibrary.simpleMessage("Sugerir recursos"), "sunrise": MessageLookupByLibrary.simpleMessage("No horizonte"), "support": MessageLookupByLibrary.simpleMessage("Suporte"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronização interrompida"), "syncing": MessageLookupByLibrary.simpleMessage("Sincronizando..."), @@ -1961,18 +1907,19 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Toque para desbloquear"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Toque para enviar"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( - "Parece que algo deu errado. Tente novamente mais tarde. Caso o erro persistir, por favor, entre em contato com nossa equipe."), - "terminate": MessageLookupByLibrary.simpleMessage("Encerrar"), - "terminateSession": MessageLookupByLibrary.simpleMessage("Sair?"), + "Parece que algo correu mal. Por favor, tente novamente mais tarde. Se o erro persistir, entre em contacto com a nossa equipa de suporte."), + "terminate": MessageLookupByLibrary.simpleMessage("Terminar"), + "terminateSession": + MessageLookupByLibrary.simpleMessage("Terminar sessão?"), "terms": MessageLookupByLibrary.simpleMessage("Termos"), "termsOfServicesTitle": MessageLookupByLibrary.simpleMessage("Termos"), "thankYou": MessageLookupByLibrary.simpleMessage("Obrigado"), - "thankYouForSubscribing": - MessageLookupByLibrary.simpleMessage("Obrigado por assinar!"), + "thankYouForSubscribing": MessageLookupByLibrary.simpleMessage( + "Obrigado pela sua subscrição!"), "theDownloadCouldNotBeCompleted": MessageLookupByLibrary.simpleMessage( - "A instalação não pôde ser concluída"), + "Não foi possível concluir o download."), "theLinkYouAreTryingToAccessHasExpired": MessageLookupByLibrary.simpleMessage( "O link que você está tentando acessar já expirou."), @@ -1982,10 +1929,10 @@ class MessageLookup extends MessageLookupByLibrary { "theme": MessageLookupByLibrary.simpleMessage("Tema"), "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( - "Estes itens serão excluídos do seu dispositivo."), - "theyAlsoGetXGb": m96, + "Estes itens serão eliminados do seu dispositivo."), + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( - "Eles serão excluídos de todos os álbuns."), + "Serão eliminados de todos os álbuns."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( "Esta ação não pode ser desfeita"), "thisAlbumAlreadyHDACollaborativeLink": @@ -1993,56 +1940,56 @@ class MessageLookup extends MessageLookupByLibrary { "Este álbum já tem um link colaborativo"), "thisCanBeUsedToRecoverYourAccountIfYou": MessageLookupByLibrary.simpleMessage( - "Isso pode ser usado para recuperar sua conta se você perder seu segundo fator"), + "Isto pode ser usado para recuperar sua conta se você perder seu segundo fator"), "thisDevice": MessageLookupByLibrary.simpleMessage("Este dispositivo"), - "thisEmailIsAlreadyInUse": MessageLookupByLibrary.simpleMessage( - "Este e-mail já está sendo usado"), + "thisEmailIsAlreadyInUse": + MessageLookupByLibrary.simpleMessage("Este email já está em uso"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( - "Esta imagem não possui dados EXIF"), + "Esta imagem não tem dados exif"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Este é você!"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Este é o seu ID de verificação"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage( "Esta semana com o passar dos anos"), - "thisWeekXYearsAgo": m98, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( - "Isso fará você sair do dispositivo a seguir:"), + "Irá desconectar a sua conta do seguinte dispositivo:"), "thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage( - "Isso fará você sair deste dispositivo!"), + "Irá desconectar a sua conta do seu dispositivo!"), "thisWillMakeTheDateAndTimeOfAllSelected": MessageLookupByLibrary.simpleMessage( "Isso fará que a data e hora de todas as fotos selecionadas fiquem iguais."), "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Isto removerá links públicos de todos os links rápidos selecionados."), - "throughTheYears": m99, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( - "Para ativar o bloqueio do aplicativo, defina uma senha de acesso no dispositivo ou bloqueie sua tela nas opções do sistema."), + "Para ativar o bloqueio de aplicações, configure o código de acesso do dispositivo ou o bloqueio de ecrã nas definições do sistema."), "toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage( - "Para ocultar uma foto ou vídeo"), + "Para ocultar uma foto ou um vídeo"), "toResetVerifyEmail": MessageLookupByLibrary.simpleMessage( - "Para redefinir sua senha, verifique seu e-mail primeiramente."), - "todaysLogs": MessageLookupByLibrary.simpleMessage("Registros de hoje"), + "Para redefinir a sua palavra-passe, verifique primeiro o seu e-mail."), + "todaysLogs": MessageLookupByLibrary.simpleMessage("Logs de hoje"), "tooManyIncorrectAttempts": MessageLookupByLibrary.simpleMessage( "Muitas tentativas incorretas"), "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Tamanho total"), - "trash": MessageLookupByLibrary.simpleMessage("Lixeira"), - "trashDaysLeft": m100, - "trim": MessageLookupByLibrary.simpleMessage("Recortar"), - "tripInYear": m101, - "tripToLocation": m102, + "trash": MessageLookupByLibrary.simpleMessage("Lixo"), + "trashDaysLeft": m101, + "trim": MessageLookupByLibrary.simpleMessage("Cortar"), + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Contatos confiáveis"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Tente novamente"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( - "Ative a cópia de segurança para automaticamente enviar arquivos adicionados à pasta do dispositivo para o Ente."), - "twitter": MessageLookupByLibrary.simpleMessage("Twitter/X"), + "Ative o backup para enviar automaticamente arquivos adicionados a esta pasta do dispositivo para o Ente."), + "twitter": MessageLookupByLibrary.simpleMessage("Twitter"), "twoMonthsFreeOnYearlyPlans": MessageLookupByLibrary.simpleMessage( "2 meses grátis em planos anuais"), "twofactor": MessageLookupByLibrary.simpleMessage("Dois fatores"), @@ -2054,23 +2001,23 @@ class MessageLookup extends MessageLookupByLibrary { "Autenticação de dois fatores"), "twofactorAuthenticationSuccessfullyReset": MessageLookupByLibrary.simpleMessage( - "Autenticação de dois fatores redefinida com sucesso"), + "Autenticação de dois fatores redefinida com êxito"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Configuração de dois fatores"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Desarquivar"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Desarquivar álbum"), - "unarchiving": MessageLookupByLibrary.simpleMessage("Desarquivando..."), + "unarchiving": MessageLookupByLibrary.simpleMessage("Desarquivar..."), "unavailableReferralCode": MessageLookupByLibrary.simpleMessage( - "Desculpe, este código está indisponível."), + "Desculpe, este código não está disponível."), "uncategorized": MessageLookupByLibrary.simpleMessage("Sem categoria"), - "unhide": MessageLookupByLibrary.simpleMessage("Desocultar"), + "unhide": MessageLookupByLibrary.simpleMessage("Mostrar"), "unhideToAlbum": - MessageLookupByLibrary.simpleMessage("Desocultar para o álbum"), + MessageLookupByLibrary.simpleMessage("Mostrar para o álbum"), "unhiding": MessageLookupByLibrary.simpleMessage("Reexibindo..."), "unhidingFilesToAlbum": MessageLookupByLibrary.simpleMessage( - "Desocultando arquivos para o álbum"), + "Desocultar ficheiros para o álbum"), "unlock": MessageLookupByLibrary.simpleMessage("Desbloquear"), "unpinAlbum": MessageLookupByLibrary.simpleMessage("Desafixar álbum"), "unselectAll": MessageLookupByLibrary.simpleMessage("Desmarcar tudo"), @@ -2080,14 +2027,14 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Atualizando seleção de pasta..."), "upgrade": MessageLookupByLibrary.simpleMessage("Atualizar"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( - "Enviando arquivos para o álbum..."), - "uploadingMultipleMemories": m106, + "Enviar ficheiros para o álbum..."), + "uploadingMultipleMemories": m107, "uploadingSingleMemory": - MessageLookupByLibrary.simpleMessage("Preservando 1 memória..."), + MessageLookupByLibrary.simpleMessage("Preservar 1 memória..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( - "Com 50% de desconto, até 4 de dezembro"), + "Até 50% de desconto, até 4 de dezembro."), "usableReferralStorageInfo": MessageLookupByLibrary.simpleMessage( "O armazenamento disponível é limitado pelo seu plano atual. O excesso de armazenamento reivindicado tornará automaticamente útil quando você atualizar seu plano."), "useAsCover": MessageLookupByLibrary.simpleMessage("Usar como capa"), @@ -2099,51 +2046,49 @@ class MessageLookup extends MessageLookupByLibrary { "useRecoveryKey": MessageLookupByLibrary.simpleMessage("Usar chave de recuperação"), "useSelectedPhoto": - MessageLookupByLibrary.simpleMessage("Usar foto selecionada"), - "usedSpace": MessageLookupByLibrary.simpleMessage("Espaço usado"), - "validTill": m107, + MessageLookupByLibrary.simpleMessage("Utilizar foto selecionada"), + "usedSpace": MessageLookupByLibrary.simpleMessage("Espaço utilizado"), + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( - "Falha na verificação. Tente novamente"), + "Falha na verificação, por favor tente novamente"), "verificationId": - MessageLookupByLibrary.simpleMessage("ID de verificação"), + MessageLookupByLibrary.simpleMessage("ID de Verificação"), "verify": MessageLookupByLibrary.simpleMessage("Verificar"), - "verifyEmail": MessageLookupByLibrary.simpleMessage("Verificar e-mail"), - "verifyEmailID": m108, + "verifyEmail": MessageLookupByLibrary.simpleMessage("Verificar email"), + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verificar chave de acesso"), "verifyPassword": - MessageLookupByLibrary.simpleMessage("Verificar senha"), - "verifying": MessageLookupByLibrary.simpleMessage("Verificando..."), + MessageLookupByLibrary.simpleMessage("Verificar palavra-passe"), + "verifying": MessageLookupByLibrary.simpleMessage("A verificar…"), "verifyingRecoveryKey": MessageLookupByLibrary.simpleMessage( "Verificando chave de recuperação..."), "videoInfo": - MessageLookupByLibrary.simpleMessage("Informações do vídeo"), + MessageLookupByLibrary.simpleMessage("Informação de Vídeo"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("vídeo"), "videoStreaming": MessageLookupByLibrary.simpleMessage("Transmissão de vídeo"), "videos": MessageLookupByLibrary.simpleMessage("Vídeos"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Ver sessões ativas"), - "viewAddOnButton": - MessageLookupByLibrary.simpleMessage("Ver complementos"), + "viewAddOnButton": MessageLookupByLibrary.simpleMessage("Ver addons"), "viewAll": MessageLookupByLibrary.simpleMessage("Ver tudo"), "viewAllExifData": MessageLookupByLibrary.simpleMessage("Ver todos os dados EXIF"), "viewLargeFiles": - MessageLookupByLibrary.simpleMessage("Arquivos grandes"), + MessageLookupByLibrary.simpleMessage("Ficheiros grandes"), "viewLargeFilesDesc": MessageLookupByLibrary.simpleMessage( - "Ver arquivos que consumem a maior parte do armazenamento."), - "viewLogs": MessageLookupByLibrary.simpleMessage("Ver registros"), + "Ver os ficheiros que estão a consumir a maior quantidade de armazenamento."), + "viewLogs": MessageLookupByLibrary.simpleMessage("Ver logs"), "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Ver chave de recuperação"), "viewer": MessageLookupByLibrary.simpleMessage("Visualizador"), - "viewersSuccessfullyAdded": m109, "visitWebToManage": MessageLookupByLibrary.simpleMessage( - "Visite o web.ente.io para gerenciar sua assinatura"), + "Visite web.ente.io para gerir a sua subscrição"), "waitingForVerification": - MessageLookupByLibrary.simpleMessage("Esperando verificação..."), + MessageLookupByLibrary.simpleMessage("Aguardando verificação..."), "waitingForWifi": MessageLookupByLibrary.simpleMessage("Aguardando Wi-Fi..."), "warning": MessageLookupByLibrary.simpleMessage("Aviso"), @@ -2151,8 +2096,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nós somos de código aberto!"), "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( - "Não suportamos a edição de fotos e álbuns que você ainda não possui"), - "weHaveSendEmailTo": m110, + "Não suportamos a edição de fotos e álbuns que ainda não possui"), + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Fraca"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bem-vindo(a) de volta!"), @@ -2161,68 +2106,65 @@ class MessageLookup extends MessageLookupByLibrary { "Um contato confiável pode ajudá-lo em recuperar seus dados."), "yearShort": MessageLookupByLibrary.simpleMessage("ano"), "yearly": MessageLookupByLibrary.simpleMessage("Anual"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Sim"), - "yesCancel": MessageLookupByLibrary.simpleMessage("Sim"), + "yesCancel": MessageLookupByLibrary.simpleMessage("Sim, cancelar"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( "Sim, converter para visualizador"), - "yesDelete": MessageLookupByLibrary.simpleMessage("Sim, excluir"), + "yesDelete": MessageLookupByLibrary.simpleMessage("Sim, apagar"), "yesDiscardChanges": - MessageLookupByLibrary.simpleMessage("Sim, descartar alterações"), + MessageLookupByLibrary.simpleMessage("Sim, rejeitar alterações"), "yesLogout": - MessageLookupByLibrary.simpleMessage("Sim, encerrar sessão"), - "yesRemove": MessageLookupByLibrary.simpleMessage("Sim, excluir"), - "yesRenew": MessageLookupByLibrary.simpleMessage("Sim"), + MessageLookupByLibrary.simpleMessage("Sim, terminar sessão"), + "yesRemove": MessageLookupByLibrary.simpleMessage("Sim, remover"), + "yesRenew": MessageLookupByLibrary.simpleMessage("Sim, Renovar"), "yesResetPerson": - MessageLookupByLibrary.simpleMessage("Sim, redefinir pessoa"), - "you": MessageLookupByLibrary.simpleMessage("Você"), - "youAndThem": m112, + MessageLookupByLibrary.simpleMessage("Sim, repor pessoa"), + "you": MessageLookupByLibrary.simpleMessage("Tu"), + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage( "Você está em um plano familiar!"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage( - "Você está na versão mais recente"), + "Está a utilizar a versão mais recente"), "youCanAtMaxDoubleYourStorage": MessageLookupByLibrary.simpleMessage( - "* Você pode duplicar seu armazenamento ao máximo"), + "* Você pode duplicar seu armazenamento no máximo"), "youCanManageYourLinksInTheShareTab": MessageLookupByLibrary.simpleMessage( - "Você pode gerenciar seus links na aba de compartilhamento."), + "Pode gerir as suas ligações no separador partilhar."), "youCanTrySearchingForADifferentQuery": MessageLookupByLibrary.simpleMessage( - "Você pode tentar buscar por outra consulta."), + "Pode tentar pesquisar uma consulta diferente."), "youCannotDowngradeToThisPlan": MessageLookupByLibrary.simpleMessage( - "Você não pode rebaixar para este plano"), + "Não é possível fazer o downgrade para este plano"), "youCannotShareWithYourself": MessageLookupByLibrary.simpleMessage( - "Você não pode compartilhar consigo mesmo"), + "Não podes partilhar contigo mesmo"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( - "Você não tem nenhum item arquivado."), - "youHaveSuccessfullyFreedUp": m113, + "Não tem nenhum item arquivado."), + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": - MessageLookupByLibrary.simpleMessage("Sua conta foi excluída"), + MessageLookupByLibrary.simpleMessage("A sua conta foi eliminada"), "yourMap": MessageLookupByLibrary.simpleMessage("Seu mapa"), "yourPlanWasSuccessfullyDowngraded": MessageLookupByLibrary.simpleMessage( - "Seu plano foi rebaixado com sucesso"), + "O seu plano foi rebaixado com sucesso"), "yourPlanWasSuccessfullyUpgraded": MessageLookupByLibrary.simpleMessage( - "Seu plano foi atualizado com sucesso"), + "O seu plano foi atualizado com sucesso"), "yourPurchaseWasSuccessful": MessageLookupByLibrary.simpleMessage( - "Sua compra foi efetuada com sucesso"), + "Sua compra foi realizada com sucesso"), "yourStorageDetailsCouldNotBeFetched": MessageLookupByLibrary.simpleMessage( - "Seus detalhes de armazenamento não puderam ser obtidos"), + "Não foi possível obter os seus dados de armazenamento"), "yourSubscriptionHasExpired": - MessageLookupByLibrary.simpleMessage("A sua assinatura expirou"), + MessageLookupByLibrary.simpleMessage("A sua subscrição expirou"), "yourSubscriptionWasUpdatedSuccessfully": MessageLookupByLibrary.simpleMessage( - "Sua assinatura foi atualizada com sucesso"), + "A sua subscrição foi actualizada com sucesso"), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage( - "O código de verificação expirou"), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage( - "Você não possui nenhum arquivo duplicado que possa ser excluído"), + "O seu código de verificação expirou"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( - "Você não tem arquivos neste álbum que possam ser excluídos"), + "Não existem ficheiros neste álbum que possam ser eliminados"), "zoomOutToSeePhotos": MessageLookupByLibrary.simpleMessage( - "Reduzir ampliação para ver as fotos") + "Diminuir o zoom para ver fotos") }; } diff --git a/mobile/lib/generated/intl/messages_pt_BR.dart b/mobile/lib/generated/intl/messages_pt_BR.dart index 7c17ac6915..cb2b94c8d2 100644 --- a/mobile/lib/generated/intl/messages_pt_BR.dart +++ b/mobile/lib/generated/intl/messages_pt_BR.dart @@ -82,6 +82,9 @@ class MessageLookup extends MessageLookupByLibrary { static String m21(count) => "${Intl.plural(count, one: 'Excluir ${count} item', other: 'Excluir ${count} itens')}"; + static String m116(count) => + "E também excluir todas as fotos (e vídeos) presente dentro desses ${count} álbuns e de todos os álbuns que eles fazem parte?"; + static String m22(currentlyDeleting, totalCount) => "Excluindo ${currentlyDeleting} / ${totalCount}"; @@ -97,223 +100,229 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} arquivos, ${formattedSize} cada"; - static String m27(newEmail) => "E-mail alterado para ${newEmail}"; + static String m27(name) => "Este e-mail já está vinculado a ${name}."; - static String m28(email) => "${email} não possui uma conta Ente."; + static String m28(newEmail) => "E-mail alterado para ${newEmail}"; - static String m29(email) => + static String m29(email) => "${email} não possui uma conta Ente."; + + static String m30(email) => "${email} não tem uma conta Ente.\n\nEnvie-os um convite para compartilhar fotos."; - static String m30(name) => "Abraçando ${name}"; + static String m31(name) => "Abraçando ${name}"; - static String m31(text) => "Fotos adicionais encontradas para ${text}"; + static String m32(text) => "Fotos adicionais encontradas para ${text}"; - static String m32(name) => "Tendo banquete com ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} deste dispositivo foi copiado com segurança"; + static String m33(name) => "Tendo banquete com ${name}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} deste dispositivo foi copiado com segurança"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} deste álbum foi copiado com segurança"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB cada vez que alguém se inscrever a um plano pago e aplicar seu código"; - static String m36(endDate) => "A avaliação grátis acaba em ${endDate}"; + static String m37(endDate) => "A avaliação grátis acaba em ${endDate}"; - static String m37(count) => + static String m38(count) => "Você ainda pode acessá${Intl.plural(count, one: '-lo', other: '-los')} no Ente se você tiver uma assinatura ativa"; - static String m38(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; - static String m39(count, formattedSize) => + static String m40(count, formattedSize) => "${Intl.plural(count, one: 'Ele pode excluído para liberar ${formattedSize}', other: 'Eles podem ser excluídos do dispositivo para liberar ${formattedSize}')}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Processando ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Caminhando com ${name}"; + static String m42(name) => "Caminhando com ${name}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} item', other: '${count} itens')}"; - static String m43(name) => "Últimos momentos com ${name}"; + static String m44(name) => "Últimos momentos com ${name}"; - static String m44(email) => + static String m45(email) => "${email} convidou você para ser um contato confiável"; - static String m45(expiryTime) => "O link expirará em ${expiryTime}"; + static String m46(expiryTime) => "O link expirará em ${expiryTime}"; - static String m46(email) => "Vincular pessoa a ${email}"; + static String m47(email) => "Vincular pessoa a ${email}"; - static String m47(personName, email) => + static String m48(personName, email) => "Isso vinculará ${personName} a ${email}"; - static String m48(count, formattedCount) => + static String m49(count, formattedCount) => "${Intl.plural(count, zero: 'sem memórias', one: '${formattedCount} memória', other: '${formattedCount} memórias')}"; - static String m49(count) => + static String m50(count) => "${Intl.plural(count, one: 'Mover item', other: 'Mover itens')}"; - static String m50(albumName) => "Movido com sucesso para ${albumName}"; + static String m51(albumName) => "Movido com sucesso para ${albumName}"; - static String m51(personName) => "Sem sugestões para ${personName}"; + static String m52(personName) => "Sem sugestões para ${personName}"; - static String m52(name) => "Não é ${name}?"; + static String m53(name) => "Não é ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Entre em contato com ${familyAdminEmail} para alterar o seu código."; - static String m54(name) => "Festejando com ${name}"; + static String m55(name) => "Festejando com ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Força da senha: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Fale com o suporte ${providerName} se você foi cobrado"; - static String m57(name, age) => "${name} tem ${age} anos!"; + static String m58(name, age) => "${name} tem ${age} anos!"; - static String m58(name, age) => "${name} terá ${age} em breve"; - - static String m59(count) => - "${Intl.plural(count, zero: 'Sem fotos', one: '1 foto', other: '${count} fotos')}"; + static String m59(name, age) => "${name} terá ${age} em breve"; static String m60(count) => + "${Intl.plural(count, zero: 'Sem fotos', one: '1 foto', other: '${count} fotos')}"; + + static String m61(count) => "${Intl.plural(count, zero: '0 fotos', one: '1 foto', other: '${count} fotos')}"; - static String m61(endDate) => + static String m62(endDate) => "Avaliação grátis válida até ${endDate}.\nVocê pode alterar para um plano pago depois."; - static String m62(toEmail) => "Envie-nos um e-mail para ${toEmail}"; + static String m63(toEmail) => "Envie-nos um e-mail para ${toEmail}"; - static String m63(toEmail) => "Envie os registros para \n${toEmail}"; + static String m64(toEmail) => "Envie os registros para \n${toEmail}"; - static String m64(name) => "Fazendo pose com ${name}"; + static String m65(name) => "Fazendo pose com ${name}"; - static String m65(folderName) => "Processando ${folderName}..."; + static String m66(folderName) => "Processando ${folderName}..."; - static String m66(storeName) => "Avalie-nos no ${storeName}"; + static String m67(storeName) => "Avalie-nos no ${storeName}"; - static String m67(name) => "Atribuído a ${name}"; + static String m68(name) => "Atribuído a ${name}"; - static String m68(days, email) => + static String m69(days, email) => "Você poderá acessar a conta após ${days} dias. Uma notificação será enviada para ${email}."; - static String m69(email) => + static String m70(email) => "Você pode recuperar a conta com e-mail ${email} por definir uma nova senha."; - static String m70(email) => "${email} está tentando recuperar sua conta."; + static String m71(email) => "${email} está tentando recuperar sua conta."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Ambos os dois ganham ${storageInGB} GB* grátis"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} será removido do álbum compartilhado\n\nQualquer foto adicionada por ele será removida."; - static String m73(endDate) => "Renovação de assinatura em ${endDate}"; + static String m74(endDate) => "Renovação de assinatura em ${endDate}"; - static String m74(name) => "Viajando de carro com ${name}"; + static String m75(name) => "Viajando de carro com ${name}"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} resultado encontrado', other: '${count} resultados encontrados')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Incompatibilidade de comprimento de seções: ${snapshotLength} != ${searchLength}"; - static String m77(count) => "${count} selecionado(s)"; + static String m117(count) => "${count} selecionado(s)"; - static String m78(count, yourCount) => + static String m78(count) => "${count} selecionado(s)"; + + static String m79(count, yourCount) => "${count} selecionado(s) (${yourCount} seus)"; - static String m79(name) => "Tirando selfies com ${name}"; - - static String m80(verificationID) => - "Aqui está meu ID de verificação para o ente.io: ${verificationID}"; + static String m80(name) => "Tirando selfies com ${name}"; static String m81(verificationID) => + "Aqui está meu ID de verificação para o ente.io: ${verificationID}"; + + static String m82(verificationID) => "Ei, você pode confirmar se este ID de verificação do ente.io é seu?: ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Código de referência do Ente: ${referralCode} \n\nAplique-o em Configurações → Geral → Referências para obter ${referralStorageInGB} GB grátis após a sua inscrição num plano pago\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Compartilhe com pessoas específicas', one: 'Compartilhado com 1 pessoa', other: 'Compartilhado com ${numberOfPeople} pessoas')}"; - static String m84(emailIDs) => "Compartilhado com ${emailIDs}"; - - static String m85(fileType) => - "Este ${fileType} será excluído do dispositivo."; + static String m85(emailIDs) => "Compartilhado com ${emailIDs}"; static String m86(fileType) => + "Este ${fileType} será excluído do dispositivo."; + + static String m87(fileType) => "Este ${fileType} está no Ente e em seu dispositivo."; - static String m87(fileType) => "Este ${fileType} será excluído do Ente."; + static String m88(fileType) => "Este ${fileType} será excluído do Ente."; - static String m88(name) => "Jogando esportes com ${name}"; + static String m89(name) => "Jogando esportes com ${name}"; - static String m89(name) => "Destacar ${name}"; + static String m90(name) => "Destacar ${name}"; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} de ${totalAmount} ${totalStorageUnit} usado"; - static String m92(id) => + static String m93(id) => "Seu ${id} já está vinculado a outra conta Ente. Se você gostaria de usar seu ${id} com esta conta, entre em contato conosco\""; - static String m93(endDate) => "Sua assinatura será cancelada em ${endDate}"; + static String m94(endDate) => "Sua assinatura será cancelada em ${endDate}"; - static String m94(completed, total) => + static String m95(completed, total) => "${completed}/${total} memórias preservadas"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "Toque para enviar, atualmente o envio é ignorado devido a ${ignoreReason}"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "Eles também recebem ${storageAmountInGB} GB"; - static String m97(email) => "Este é o ID de verificação de ${email}"; + static String m98(email) => "Este é o ID de verificação de ${email}"; - static String m98(count) => + static String m99(count) => "${Intl.plural(count, one: 'Esta semana, ${count} ano atrás', other: 'Esta semana, ${count} anos atrás')}"; - static String m99(dateFormat) => "${dateFormat} com o passar dos anos"; + static String m100(dateFormat) => "${dateFormat} com o passar dos anos"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Em breve', one: '1 dia', other: '${count} dias')}"; - static String m101(year) => "Viajem em ${year}"; + static String m102(year) => "Viajem em ${year}"; - static String m102(location) => "Viajem à ${location}"; + static String m103(location) => "Viajem à ${location}"; - static String m103(email) => + static String m104(email) => "Você foi convidado para ser um contato legado por ${email}."; - static String m104(galleryType) => + static String m105(galleryType) => "O tipo de galeria ${galleryType} não é suportado para renomear"; - static String m105(ignoreReason) => + static String m106(ignoreReason) => "O envio é ignorado devido a ${ignoreReason}"; - static String m106(count) => "Preservando ${count} memórias..."; + static String m107(count) => "Preservando ${count} memórias..."; - static String m107(endDate) => "Válido até ${endDate}"; + static String m108(endDate) => "Válido até ${endDate}"; - static String m108(email) => "Verificar ${email}"; + static String m109(email) => "Verificar ${email}"; - static String m109(count) => - "${Intl.plural(count, zero: 'Adicionado 0 vizualizadores', one: 'Adicionado 1 visualizador', other: 'Adicionado ${count} visualizadores')}"; - - static String m110(email) => "Enviamos um e-mail à ${email}"; + static String m110(name) => "Visualizar ${name} para desvincular"; static String m111(count) => + "${Intl.plural(count, zero: 'Adicionado 0 vizualizadores', one: 'Adicionado 1 visualizador', other: 'Adicionado ${count} visualizadores')}"; + + static String m112(email) => "Enviamos um e-mail à ${email}"; + + static String m113(count) => "${Intl.plural(count, one: '${count} ano atrás', other: '${count} anos atrás')}"; - static String m112(name) => "Você e ${name}"; + static String m114(name) => "Você e ${name}"; - static String m113(storageSaved) => + static String m115(storageSaved) => "Você liberou ${storageSaved} com sucesso!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -331,6 +340,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bem-vindo(a) de volta!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( "Eu entendo que se eu perder minha senha, posso perder meus dados, já que meus dados são criptografados de ponta a ponta."), + "actionNotSupportedOnFavouritesAlbum": + MessageLookupByLibrary.simpleMessage( + "Ação não suportada em álbum favorito"), "activeSessions": MessageLookupByLibrary.simpleMessage("Sessões ativas"), "add": MessageLookupByLibrary.simpleMessage("Adicionar"), @@ -358,6 +370,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Detalhes dos complementos"), "addOnValidTill": m3, "addOns": MessageLookupByLibrary.simpleMessage("Complementos"), + "addParticipants": + MessageLookupByLibrary.simpleMessage("Adicionar participante"), "addPhotos": MessageLookupByLibrary.simpleMessage("Adicionar fotos"), "addSelected": MessageLookupByLibrary.simpleMessage("Adicionar selecionado"), @@ -545,23 +559,6 @@ class MessageLookup extends MessageLookupByLibrary { "blackFridaySale": MessageLookupByLibrary.simpleMessage("Promoção Black Friday"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLBulkEdit": - MessageLookupByLibrary.simpleMessage("Editar todas as datas"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Agora você pode selecionar várias fotos, editar data e hora de todos com um só clique. Alternar datas também são suportados."), - "cLFamilyPlan": MessageLookupByLibrary.simpleMessage( - "Limites de planos familiares"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Agora você pode definir um limite de quanto armazenamento os seus entes queridos podem usar."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Novo Ícone"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Finalmente, um novo ícone para o ente que acreditamos que represente melhor nosso trabalho. Também, adicionamos um alterador de ícone para que você ainda consiga utilizar o ícone antigo."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Memórias"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Relembre momentos especiais - destaque pessoas favoritas, suas viagens e feriados, melhores fotos, e muito mais. Ative o aprendizado automático, marque-se e nomeie seus amigos para melhorar a experiência."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widgets"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Widgets integrados com memórias já estão disponíveis. Eles apareceram com seus melhores momentos sem precisar abrir o ente."), "cachedData": MessageLookupByLibrary.simpleMessage("Dados armazenados em cache"), "calculating": MessageLookupByLibrary.simpleMessage("Calculando..."), @@ -775,6 +772,7 @@ class MessageLookup extends MessageLookupByLibrary { "deleteItemCount": m21, "deleteLocation": MessageLookupByLibrary.simpleMessage("Excluir localização"), + "deleteMultipleAlbumDialog": m116, "deletePhotos": MessageLookupByLibrary.simpleMessage("Excluir fotos"), "deleteProgress": m22, "deleteReason1": MessageLookupByLibrary.simpleMessage( @@ -865,6 +863,7 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m25, "duplicateItemsGroup": m26, "edit": MessageLookupByLibrary.simpleMessage("Editar"), + "editEmailAlreadyLinked": m27, "editLocation": MessageLookupByLibrary.simpleMessage("Editar localização"), "editLocationTagTitle": @@ -879,16 +878,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-mail"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("E-mail já registrado."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("E-mail não registrado."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Verificação por e-mail"), "emailYourLogs": MessageLookupByLibrary.simpleMessage("Enviar registros por e-mail"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Contatos de emergência"), "empty": MessageLookupByLibrary.simpleMessage("Esvaziar"), @@ -949,6 +948,8 @@ class MessageLookup extends MessageLookupByLibrary { "Insira um endereço de e-mail válido."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage( "Insira seu endereço de e-mail"), + "enterYourNewEmailAddress": + MessageLookupByLibrary.simpleMessage("Insira seu novo e-mail"), "enterYourPassword": MessageLookupByLibrary.simpleMessage("Insira sua senha"), "enterYourRecoveryKey": MessageLookupByLibrary.simpleMessage( @@ -967,7 +968,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exportar dados"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Fotos adicionais encontradas"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Rosto não agrupado ainda, volte aqui mais tarde"), "faceRecognition": @@ -1006,7 +1007,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("Perguntas frequentes"), "faqs": MessageLookupByLibrary.simpleMessage("Perguntas frequentes"), "favorite": MessageLookupByLibrary.simpleMessage("Favorito"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Feedback"), "file": MessageLookupByLibrary.simpleMessage("Arquivo"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -1020,8 +1021,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Tipos de arquivo"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Arquivos excluídos"), "filesSavedToGallery": @@ -1040,26 +1041,26 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Rostos encontrados"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Armaz. grátis reivindicado"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Armazenamento disponível"), "freeTrial": MessageLookupByLibrary.simpleMessage("Avaliação grátis"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Liberar espaço no dispositivo"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Economize espaço em seu dispositivo por limpar arquivos já salvos com segurança."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Liberar espaço"), - "freeUpSpaceSaving": m39, + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Galeria"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Até 1.000 memórias exibidas na galeria"), "general": MessageLookupByLibrary.simpleMessage("Geral"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Gerando chaves de criptografia..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Ir às opções"), "googlePlayId": MessageLookupByLibrary.simpleMessage("ID do Google Play"), @@ -1088,7 +1089,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Ocultar itens compartilhados da galeria inicial"), "hiding": MessageLookupByLibrary.simpleMessage("Ocultando..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("Hospedado em OSM France"), "howItWorks": MessageLookupByLibrary.simpleMessage("Como funciona"), @@ -1145,7 +1146,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Parece que algo deu errado. Tente novamente mais tarde. Caso o erro persistir, por favor, entre em contato com nossa equipe."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Os itens exibem o número de dias restantes antes da exclusão permanente"), @@ -1166,7 +1167,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Ajude-nos com esta informação"), "language": MessageLookupByLibrary.simpleMessage("Idioma"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Última atualização"), "lastYearsTrip": @@ -1181,7 +1182,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Legado"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Contas legadas"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "O legado permite que contatos confiáveis acessem sua conta em sua ausência."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1198,7 +1199,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("para compartilhar rápido"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Ativado"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expirado"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Expiração do link"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("O link expirou"), @@ -1206,8 +1207,8 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Vincular pessoa"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "para melhorar o compartilhamento"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Fotos animadas"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Você pode compartilhar sua assinatura com seus familiares"), @@ -1298,7 +1299,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Eu"), - "memoryCount": m48, + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Produtos"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Juntar com o existente"), @@ -1329,14 +1330,14 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Mais recente"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Mais relevante"), "mountains": MessageLookupByLibrary.simpleMessage("Sob as montanhas"), - "moveItem": m49, + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Mover fotos selecionadas para uma data"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Mover para o álbum"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Mover ao álbum oculto"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Movido para a lixeira"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1390,10 +1391,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Nenhum resultado"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Nenhum resultado encontrado"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Nenhum bloqueio do sistema encontrado"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("Não é esta pessoa?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( @@ -1407,7 +1408,8 @@ class MessageLookup extends MessageLookupByLibrary { "No ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("Na estrada novamente"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("Neste dia"), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Apenas eles"), "oops": MessageLookupByLibrary.simpleMessage("Ops"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1437,7 +1439,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("Pareamento concluído"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage("Verificação pendente"), "passkey": MessageLookupByLibrary.simpleMessage("Chave de acesso"), @@ -1448,7 +1450,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Senha alterada com sucesso"), "passwordLock": MessageLookupByLibrary.simpleMessage("Bloqueio por senha"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "A força da senha é calculada considerando o comprimento dos dígitos, carácteres usados, e se ou não a senha aparece nas 10.000 senhas usadas."), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1459,7 +1461,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("O pagamento falhou"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Infelizmente o pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"), "pendingSync": MessageLookupByLibrary.simpleMessage("Sincronização pendente"), @@ -1472,21 +1474,21 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Excluir permanentemente"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "Excluir permanentemente do dispositivo?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Nome da pessoa"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Companhias peludas"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Descrições das fotos"), "photoGridSize": MessageLookupByLibrary.simpleMessage("Tamanho da grade de fotos"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("foto"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Fotos"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Suas fotos adicionadas serão removidas do álbum"), - "photosCount": m60, + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "As fotos mantêm a diferença de tempo relativo"), @@ -1498,7 +1500,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Reproduzir álbum na TV"), "playOriginal": MessageLookupByLibrary.simpleMessage("Reproduzir original"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Reproduzir transmissão"), "playstoreSubscription": @@ -1512,14 +1514,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Por favor, contate o suporte se o problema persistir"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Por favor, conceda as permissões"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Registre-se novamente"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Selecione links rápidos para remover"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Tente novamente"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1532,7 +1534,7 @@ class MessageLookup extends MessageLookupByLibrary { "Por favor, aguarde mais algum tempo antes de tentar novamente"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Aguarde um pouco, isso talvez leve um tempo."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("Preparando registros..."), "preserveMore": MessageLookupByLibrary.simpleMessage("Preservar mais"), @@ -1551,7 +1553,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Continuar"), "processed": MessageLookupByLibrary.simpleMessage("Processado"), "processing": MessageLookupByLibrary.simpleMessage("Processando"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Processando vídeos"), "publicLinkCreated": @@ -1565,9 +1567,9 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Avalie o aplicativo"), "rateUs": MessageLookupByLibrary.simpleMessage("Avaliar"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("Reatribuir \"Eu\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Reatribuindo..."), "recover": MessageLookupByLibrary.simpleMessage("Recuperar"), @@ -1578,7 +1580,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recuperar conta"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("A recuperação iniciou"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Chave de recuperação"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1593,12 +1595,12 @@ class MessageLookup extends MessageLookupByLibrary { "Chave de recuperação verificada"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Sua chave de recuperação é a única maneira de recuperar suas fotos se você esqueceu sua senha. Você pode encontrar sua chave de recuperação em Opções > Conta.\n\nInsira sua chave de recuperação aqui para verificar se você a salvou corretamente."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Recuperação com sucesso!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Um contato confiável está tentando acessar sua conta"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "O dispositivo atual não é poderoso o suficiente para verificar sua senha, no entanto, nós podemos regenerar numa maneira que funciona em todos os dispositivos.\n\nEntre usando a chave de recuperação e regenere sua senha (você pode usar a mesma novamente se desejar)."), "recreatePasswordTitle": @@ -1613,7 +1615,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Envie este código aos seus amigos"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Eles então se inscrevem num plano pago"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Referências"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "As referências estão atualmente pausadas"), @@ -1642,7 +1644,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Remover link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Remover participante"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Remover etiqueta da pessoa"), "removePublicLink": @@ -1662,7 +1664,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Renomear arquivo"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renovar assinatura"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Informar um erro"), "reportBug": MessageLookupByLibrary.simpleMessage("Informar erro"), "resendEmail": MessageLookupByLibrary.simpleMessage("Reenviar e-mail"), @@ -1687,7 +1689,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Revisar sugestões"), "right": MessageLookupByLibrary.simpleMessage("Direita"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Girar"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Girar para a esquerda"), @@ -1745,8 +1747,8 @@ class MessageLookup extends MessageLookupByLibrary { "Convide pessoas e você verá todas as fotos compartilhadas por elas aqui"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "As pessoas serão exibidas aqui quando o processamento e sincronização for concluído"), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Segurança"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Ver links de álbum compartilhado no aplicativo"), @@ -1784,6 +1786,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Selecione seu rosto"), "selectYourPlan": MessageLookupByLibrary.simpleMessage("Selecione seu plano"), + "selectedAlbums": m117, "selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage( "Os arquivos selecionados não estão no Ente"), "selectedFoldersWillBeEncryptedAndBackedUp": @@ -1795,9 +1798,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Os itens selecionados serão removidos desta pessoa, entretanto não serão excluídos da sua biblioteca."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, - "selfiesWithThem": m79, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Enviar"), "sendEmail": MessageLookupByLibrary.simpleMessage("Enviar e-mail"), "sendInvite": MessageLookupByLibrary.simpleMessage("Enviar convite"), @@ -1827,16 +1830,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Compartilhar um álbum agora"), "shareLink": MessageLookupByLibrary.simpleMessage("Compartilhar link"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Compartilhar apenas com as pessoas que você quiser"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Baixe o Ente para que nós possamos compartilhar com facilidade fotos e vídeos de qualidade original\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Compartilhar com usuários não ente"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Compartilhar seu primeiro álbum"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1849,7 +1852,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Novas fotos compartilhadas"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Receber notificações quando alguém adicionar uma foto a um álbum compartilhado que você faz parte"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Compartilhado comigo"), "sharedWithYou": @@ -1868,11 +1871,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sair em outros dispositivos"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Eu concordo com os termos de serviço e a política de privacidade"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Ele será excluído de todos os álbuns."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Pular"), "social": MessageLookupByLibrary.simpleMessage("Redes sociais"), "someItemsAreInBothEnteAndYourDevice": @@ -1910,8 +1913,8 @@ class MessageLookup extends MessageLookupByLibrary { "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Antigos primeiro"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Sucesso"), - "sportsWithThem": m88, - "spotlightOnThem": m89, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Destacar si mesmo"), "startAccountRecoveryTitle": @@ -1926,15 +1929,15 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Armazenamento"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Família"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Você"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Limite de armazenamento excedido"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Detalhes da transmissão"), "strongStrength": MessageLookupByLibrary.simpleMessage("Forte"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Inscrever-se"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Você precisa de uma inscrição paga ativa para ativar o compartilhamento."), @@ -1952,7 +1955,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sugerir recurso"), "sunrise": MessageLookupByLibrary.simpleMessage("No horizonte"), "support": MessageLookupByLibrary.simpleMessage("Suporte"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronização interrompida"), "syncing": MessageLookupByLibrary.simpleMessage("Sincronizando..."), @@ -1964,7 +1967,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Toque para desbloquear"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Toque para enviar"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Parece que algo deu errado. Tente novamente mais tarde. Caso o erro persistir, por favor, entre em contato com nossa equipe."), "terminate": MessageLookupByLibrary.simpleMessage("Encerrar"), @@ -1986,7 +1989,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Estes itens serão excluídos do seu dispositivo."), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Eles serão excluídos de todos os álbuns."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -2004,12 +2007,12 @@ class MessageLookup extends MessageLookupByLibrary { "Esta imagem não possui dados EXIF"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Este é você!"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Este é o seu ID de verificação"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage( "Esta semana com o passar dos anos"), - "thisWeekXYearsAgo": m98, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Isso fará você sair do dispositivo a seguir:"), @@ -2021,7 +2024,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Isto removerá links públicos de todos os links rápidos selecionados."), - "throughTheYears": m99, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "Para ativar o bloqueio do aplicativo, defina uma senha de acesso no dispositivo ou bloqueie sua tela nas opções do sistema."), @@ -2035,13 +2038,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Tamanho total"), "trash": MessageLookupByLibrary.simpleMessage("Lixeira"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Recortar"), - "tripInYear": m101, - "tripToLocation": m102, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Contatos confiáveis"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Tente novamente"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Ative a cópia de segurança para automaticamente enviar arquivos adicionados à pasta do dispositivo para o Ente."), @@ -2060,7 +2063,7 @@ class MessageLookup extends MessageLookupByLibrary { "Autenticação de dois fatores redefinida com sucesso"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Configuração de dois fatores"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Desarquivar"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Desarquivar álbum"), @@ -2083,10 +2086,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Atualizando seleção de pasta..."), "upgrade": MessageLookupByLibrary.simpleMessage("Atualizar"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Enviando arquivos para o álbum..."), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Preservando 1 memória..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -2104,7 +2107,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Usar foto selecionada"), "usedSpace": MessageLookupByLibrary.simpleMessage("Espaço usado"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Falha na verificação. Tente novamente"), @@ -2112,7 +2115,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ID de verificação"), "verify": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verificar e-mail"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verificar chave de acesso"), @@ -2125,7 +2128,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Informações do vídeo"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("vídeo"), "videoStreaming": - MessageLookupByLibrary.simpleMessage("Transmissão de vídeo"), + MessageLookupByLibrary.simpleMessage("Vídeos transmissíveis"), "videos": MessageLookupByLibrary.simpleMessage("Vídeos"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Ver sessões ativas"), @@ -2139,10 +2142,11 @@ class MessageLookup extends MessageLookupByLibrary { "viewLargeFilesDesc": MessageLookupByLibrary.simpleMessage( "Ver arquivos que consumem a maior parte do armazenamento."), "viewLogs": MessageLookupByLibrary.simpleMessage("Ver registros"), + "viewPersonToUnlink": m110, "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Ver chave de recuperação"), "viewer": MessageLookupByLibrary.simpleMessage("Visualizador"), - "viewersSuccessfullyAdded": m109, + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Visite o web.ente.io para gerenciar sua assinatura"), "waitingForVerification": @@ -2155,7 +2159,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Não suportamos a edição de fotos e álbuns que você ainda não possui"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Fraca"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bem-vindo(a) de volta!"), @@ -2164,7 +2168,7 @@ class MessageLookup extends MessageLookupByLibrary { "Um contato confiável pode ajudá-lo em recuperar seus dados."), "yearShort": MessageLookupByLibrary.simpleMessage("ano"), "yearly": MessageLookupByLibrary.simpleMessage("Anual"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Sim"), "yesCancel": MessageLookupByLibrary.simpleMessage("Sim"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -2179,7 +2183,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage("Sim, redefinir pessoa"), "you": MessageLookupByLibrary.simpleMessage("Você"), - "youAndThem": m112, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage( "Você está em um plano familiar!"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage( @@ -2198,7 +2202,7 @@ class MessageLookup extends MessageLookupByLibrary { "Não é possível compartilhar consigo mesmo"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Você não tem nenhum item arquivado."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Sua conta foi excluída"), "yourMap": MessageLookupByLibrary.simpleMessage("Seu mapa"), diff --git a/mobile/lib/generated/intl/messages_pt_PT.dart b/mobile/lib/generated/intl/messages_pt_PT.dart index 706d5f7f43..bf8d3d318f 100644 --- a/mobile/lib/generated/intl/messages_pt_PT.dart +++ b/mobile/lib/generated/intl/messages_pt_PT.dart @@ -79,136 +79,136 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} arquivos, ${formattedSize} cada"; - static String m27(newEmail) => "Email alterado para ${newEmail}"; + static String m28(newEmail) => "Email alterado para ${newEmail}"; - static String m29(email) => + static String m30(email) => "${email} não possui uma conta Ente.\n\nEnvie um convite para compartilhar fotos."; - static String m31(text) => "Fotos extras encontradas para ${text}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} neste dispositivo teve um backup seguro"; + static String m32(text) => "Fotos extras encontradas para ${text}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} neste dispositivo teve um backup seguro"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} neste álbum teve um backup seguro"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB sempre que alguém se inscreve num plano pago e aplica o seu código"; - static String m36(endDate) => "Teste gratuito válido até ${endDate}"; + static String m37(endDate) => "Teste gratuito válido até ${endDate}"; - static String m38(sizeInMBorGB) => "Libertar ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Libertar ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Processando ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} item', other: '${count} itens')}"; - static String m45(expiryTime) => "O link expirará em ${expiryTime}"; + static String m46(expiryTime) => "O link expirará em ${expiryTime}"; - static String m50(albumName) => "Movido com sucesso para ${albumName}"; + static String m51(albumName) => "Movido com sucesso para ${albumName}"; - static String m52(name) => "Não é ${name}?"; + static String m53(name) => "Não é ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Entre em contato com ${familyAdminEmail} para alterar o seu código."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Força da palavra-passe: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Por favor, fale com o suporte ${providerName} se você foi cobrado"; - static String m61(endDate) => + static String m62(endDate) => "Teste gratuito válido até ${endDate}.\nVocê pode escolher um plano pago depois."; - static String m62(toEmail) => + static String m63(toEmail) => "Por favor, envie-nos um e-mail para ${toEmail}"; - static String m63(toEmail) => "Por favor, envie os logs para \n${toEmail}"; + static String m64(toEmail) => "Por favor, envie os logs para \n${toEmail}"; - static String m65(folderName) => "Processando ${folderName}..."; + static String m66(folderName) => "Processando ${folderName}..."; - static String m66(storeName) => "Avalie-nos em ${storeName}"; + static String m67(storeName) => "Avalie-nos em ${storeName}"; - static String m71(storageInGB) => "3. Ambos ganham ${storageInGB} GB* grátis"; + static String m72(storageInGB) => "3. Ambos ganham ${storageInGB} GB* grátis"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por elas também serão removidas do álbum"; - static String m73(endDate) => "A subscrição é renovada em ${endDate}"; + static String m74(endDate) => "A subscrição é renovada em ${endDate}"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} ano atrás', other: '${count} anos atrás')}"; - static String m77(count) => "${count} selecionado(s)"; + static String m78(count) => "${count} selecionado(s)"; - static String m78(count, yourCount) => + static String m79(count, yourCount) => "${count} selecionado(s) (${yourCount} seus)"; - static String m80(verificationID) => + static String m81(verificationID) => "Aqui está o meu ID de verificação: ${verificationID} para ente.io."; - static String m81(verificationID) => + static String m82(verificationID) => "Ei, você pode confirmar que este é seu ID de verificação do ente.io: ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Insira o código de referência: ${referralCode} \n\nAplique-o em Configurações → Geral → Indicações para obter ${referralStorageInGB} GB gratuitamente após a sua inscrição para um plano pago\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Compartilhe com pessoas específicas', one: 'Compartilhado com 1 pessoa', other: 'Compartilhado com ${numberOfPeople} pessoas')}"; - static String m84(emailIDs) => "Partilhado com ${emailIDs}"; - - static String m85(fileType) => - "Este ${fileType} será eliminado do seu dispositivo."; + static String m85(emailIDs) => "Partilhado com ${emailIDs}"; static String m86(fileType) => + "Este ${fileType} será eliminado do seu dispositivo."; + + static String m87(fileType) => "Este ${fileType} encontra-se tanto no Ente como no seu dispositivo."; - static String m87(fileType) => "Este ${fileType} será eliminado do Ente."; + static String m88(fileType) => "Este ${fileType} será eliminado do Ente."; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} de ${totalAmount} ${totalStorageUnit} usado"; - static String m92(id) => + static String m93(id) => "Seu ${id} já está vinculado a outra conta Ente.\nSe você gostaria de usar seu ${id} com esta conta, por favor contate nosso suporte\'\'"; - static String m93(endDate) => "A sua subscrição será cancelada em ${endDate}"; + static String m94(endDate) => "A sua subscrição será cancelada em ${endDate}"; - static String m94(completed, total) => + static String m95(completed, total) => "${completed}/${total} memórias preservadas"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "Eles também recebem ${storageAmountInGB} GB"; - static String m97(email) => "Este é o ID de verificação de ${email}"; + static String m98(email) => "Este é o ID de verificação de ${email}"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Brevemente', one: '1 dia', other: '${count} dias')}"; - static String m104(galleryType) => + static String m105(galleryType) => "Tipo de galeria ${galleryType} não é permitido para renomear"; - static String m105(ignoreReason) => "Envio ignorado devido à ${ignoreReason}"; + static String m106(ignoreReason) => "Envio ignorado devido à ${ignoreReason}"; - static String m106(count) => "Preservar ${count} memórias..."; + static String m107(count) => "Preservar ${count} memórias..."; - static String m107(endDate) => "Válido até ${endDate}"; + static String m108(endDate) => "Válido até ${endDate}"; - static String m108(email) => "Verificar e-mail"; + static String m109(email) => "Verificar e-mail"; - static String m110(email) => + static String m112(email) => "Enviamos um e-mail para ${email}"; - static String m111(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} ano atrás', other: '${count} anos atrás')}"; - static String m113(storageSaved) => + static String m115(storageSaved) => "Você liberou ${storageSaved} com sucesso!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -710,8 +710,8 @@ class MessageLookup extends MessageLookupByLibrary { "Edições para localização só serão vistas dentro do Ente"), "eligible": MessageLookupByLibrary.simpleMessage("elegível"), "email": MessageLookupByLibrary.simpleMessage("Email"), - "emailChangedTo": m27, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailNoEnteAccount": m30, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Verificação por e-mail"), "emailYourLogs": @@ -790,7 +790,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exportar os seus dados"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Fotos adicionais encontradas"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceRecognition": MessageLookupByLibrary.simpleMessage("Reconhecimento facial"), "faces": MessageLookupByLibrary.simpleMessage("Rostos"), @@ -836,8 +836,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Tipos de arquivo"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Arquivos apagados"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( @@ -855,12 +855,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Rostos encontrados"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Armazenamento gratuito reclamado"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Armazenamento livre utilizável"), "freeTrial": MessageLookupByLibrary.simpleMessage("Teste grátis"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Libertar espaço no dispositivo"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -871,7 +871,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("Geral"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Gerando chaves de encriptação..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Ir para as definições"), "googlePlayId": @@ -949,7 +949,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Parece que algo correu mal. Por favor, tente novamente após algum tempo. Se o erro persistir, contacte a nossa equipa de apoio."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Os itens mostram o número de dias restantes antes da eliminação permanente"), @@ -979,7 +979,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Limite de dispositivo"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Ativado"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expirado"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Link expirado"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("O link expirou"), @@ -1101,7 +1101,7 @@ class MessageLookup extends MessageLookupByLibrary { "moveToAlbum": MessageLookupByLibrary.simpleMessage("Mover para álbum"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Mover para álbum oculto"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Mover para o lixo"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1151,7 +1151,7 @@ class MessageLookup extends MessageLookupByLibrary { "Não foram encontrados resultados"), "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Nenhum bloqueio de sistema encontrado"), - "notPersonLabel": m52, + "notPersonLabel": m53, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Ainda nada partilhado consigo"), "nothingToSeeHere": @@ -1161,7 +1161,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("No dispositivo"), "onEnte": MessageLookupByLibrary.simpleMessage( "Em ente"), - "onlyFamilyAdminCanChangeCode": m53, + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Apenas eles"), "oops": MessageLookupByLibrary.simpleMessage("Oops"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1195,7 +1195,7 @@ class MessageLookup extends MessageLookupByLibrary { "Palavra-passe alterada com sucesso"), "passwordLock": MessageLookupByLibrary.simpleMessage("Bloqueio da palavra-passe"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "A força da palavra-passe é calculada tendo em conta o comprimento da palavra-passe, os caracteres utilizados e se a palavra-passe aparece ou não nas 10.000 palavras-passe mais utilizadas"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1206,7 +1206,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("O pagamento falhou"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Infelizmente o seu pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"), "pendingSync": MessageLookupByLibrary.simpleMessage("Sincronização pendente"), @@ -1235,7 +1235,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("Bloqueio por PIN"), "playOnTv": MessageLookupByLibrary.simpleMessage("Reproduzir álbum na TV"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Subscrição da PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1247,14 +1247,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Por favor, contate o suporte se o problema persistir"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Por favor, conceda as permissões"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage( "Por favor, inicie sessão novamente"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Selecione links rápidos para remover"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Por favor, tente novamente"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1281,7 +1281,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Backups privados"), "privateSharing": MessageLookupByLibrary.simpleMessage("Partilha privada"), - "processingImport": m65, + "processingImport": m66, "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Link público criado"), "publicLinkEnabled": @@ -1291,7 +1291,7 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Abrir ticket"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Avaliar aplicação"), "rateUs": MessageLookupByLibrary.simpleMessage("Avalie-nos"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "recover": MessageLookupByLibrary.simpleMessage("Recuperar"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recuperar conta"), @@ -1327,7 +1327,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Envie este código aos seus amigos"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Eles se inscrevem em um plano pago"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Referências"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "As referências estão atualmente em pausa"), @@ -1353,7 +1353,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Remover link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Remover participante"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Remover etiqueta da pessoa"), "removePublicLink": @@ -1371,7 +1371,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Renomear arquivo"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renovar subscrição"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Reporte um bug"), "reportBug": MessageLookupByLibrary.simpleMessage("Reportar bug"), "resendEmail": MessageLookupByLibrary.simpleMessage("Reenviar e-mail"), @@ -1446,7 +1446,7 @@ class MessageLookup extends MessageLookupByLibrary { "Fotos de grupo que estão sendo tiradas em algum raio da foto"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( "Convide pessoas e verá todas as fotos partilhadas por elas aqui"), - "searchResultCount": m75, + "searchResultCount": m76, "security": MessageLookupByLibrary.simpleMessage("Segurança"), "selectALocation": MessageLookupByLibrary.simpleMessage("Selecione uma localização"), @@ -1474,8 +1474,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Os itens selecionados serão eliminados de todos os álbuns e movidos para o lixo."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "send": MessageLookupByLibrary.simpleMessage("Enviar"), "sendEmail": MessageLookupByLibrary.simpleMessage("Enviar email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Enviar convite"), @@ -1506,16 +1506,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Partilhar um álbum"), "shareLink": MessageLookupByLibrary.simpleMessage("Partilhar link"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Partilhar apenas com as pessoas que deseja"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Descarregue o Ente para poder partilhar facilmente fotografias e vídeos de qualidade original\n\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Compartilhar com usuários que não usam Ente"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Partilhe o seu primeiro álbum"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1528,7 +1528,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Novas fotos partilhadas"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Receber notificações quando alguém adiciona uma foto a um álbum partilhado do qual faz parte"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Partilhado comigo"), "sharedWithYou": @@ -1545,11 +1545,11 @@ class MessageLookup extends MessageLookupByLibrary { "Terminar a sessão noutros dispositivos"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Eu concordo com os termos de serviço e política de privacidade"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Será eliminado de todos os álbuns."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Pular"), "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": @@ -1595,13 +1595,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Armazenamento"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Família"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Tu"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Limite de armazenamento excedido"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("Forte"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Subscrever"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Você precisa de uma assinatura paga ativa para ativar o compartilhamento."), @@ -1618,7 +1618,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Sugerir recursos"), "support": MessageLookupByLibrary.simpleMessage("Suporte"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronização interrompida"), "syncing": MessageLookupByLibrary.simpleMessage("Sincronizando..."), @@ -1647,7 +1647,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Estes itens serão eliminados do seu dispositivo."), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Serão eliminados de todos os álbuns."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1663,7 +1663,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Este email já está em uso"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Esta imagem não tem dados exif"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Este é o seu ID de verificação"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1687,7 +1687,7 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Tamanho total"), "trash": MessageLookupByLibrary.simpleMessage("Lixo"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Cortar"), "tryAgain": MessageLookupByLibrary.simpleMessage("Tente novamente"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( @@ -1707,7 +1707,7 @@ class MessageLookup extends MessageLookupByLibrary { "Autenticação de dois fatores redefinida com êxito"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Configuração de dois fatores"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Desarquivar"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Desarquivar álbum"), @@ -1730,10 +1730,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Atualizando seleção de pasta..."), "upgrade": MessageLookupByLibrary.simpleMessage("Atualizar"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Enviar ficheiros para o álbum..."), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Preservar 1 memória..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1749,7 +1749,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Utilizar foto selecionada"), "usedSpace": MessageLookupByLibrary.simpleMessage("Espaço utilizado"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Falha na verificação, por favor tente novamente"), @@ -1757,7 +1757,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ID de Verificação"), "verify": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verificar email"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verificar chave de acesso"), @@ -1795,13 +1795,13 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Não suportamos a edição de fotos e álbuns que ainda não possui"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Fraca"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bem-vindo(a) de volta!"), "whatsNew": MessageLookupByLibrary.simpleMessage("O que há de novo"), "yearly": MessageLookupByLibrary.simpleMessage("Anual"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Sim"), "yesCancel": MessageLookupByLibrary.simpleMessage("Sim, cancelar"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -1834,7 +1834,7 @@ class MessageLookup extends MessageLookupByLibrary { "Não podes partilhar contigo mesmo"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Não tem nenhum item arquivado."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("A sua conta foi eliminada"), "yourMap": MessageLookupByLibrary.simpleMessage("Seu mapa"), diff --git a/mobile/lib/generated/intl/messages_ro.dart b/mobile/lib/generated/intl/messages_ro.dart index f9102e981b..f75cb968d7 100644 --- a/mobile/lib/generated/intl/messages_ro.dart +++ b/mobile/lib/generated/intl/messages_ro.dart @@ -80,162 +80,162 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} fișiere, ${formattedSize} fiecare"; - static String m27(newEmail) => "E-mail modificat în ${newEmail}"; + static String m28(newEmail) => "E-mail modificat în ${newEmail}"; - static String m29(email) => + static String m30(email) => "${email} nu are un cont Ente.\n\nTrimiteți-le o invitație pentru a distribui fotografii."; - static String m31(text) => "S-au găsit fotografii extra pentru ${text}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: 'Un fișier de pe acest dispozitiv a fost deja salvat în siguranță', few: '${formattedNumber} fișiere de pe acest dispozitiv au fost deja salvate în siguranță', other: '${formattedNumber} de fișiere de pe acest dispozitiv fost deja salvate în siguranță')}"; + static String m32(text) => "S-au găsit fotografii extra pentru ${text}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: 'Un fișier de pe acest dispozitiv a fost deja salvat în siguranță', few: '${formattedNumber} fișiere de pe acest dispozitiv au fost deja salvate în siguranță', other: '${formattedNumber} de fișiere de pe acest dispozitiv fost deja salvate în siguranță')}"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: 'Un fișier din acest album a fost deja salvat în siguranță', few: '${formattedNumber} fișiere din acest album au fost deja salvate în siguranță', other: '${formattedNumber} de fișiere din acest album au fost deja salvate în siguranță')}"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB de fiecare dată când cineva se înscrie pentru un plan plătit și aplică codul dvs."; - static String m36(endDate) => + static String m37(endDate) => "Perioadă de încercare valabilă până pe ${endDate}"; - static String m38(sizeInMBorGB) => "Eliberați ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Eliberați ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Se procesează ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} articol', few: '${count} articole', other: '${count} de articole')}"; - static String m44(email) => + static String m45(email) => "${email} v-a invitat să fiți un contact de încredere"; - static String m45(expiryTime) => "Linkul va expira pe ${expiryTime}"; + static String m46(expiryTime) => "Linkul va expira pe ${expiryTime}"; - static String m50(albumName) => "S-au mutat cu succes în ${albumName}"; + static String m51(albumName) => "S-au mutat cu succes în ${albumName}"; - static String m51(personName) => "Nicio sugestie pentru ${personName}"; + static String m52(personName) => "Nicio sugestie pentru ${personName}"; - static String m52(name) => "Nu este ${name}?"; + static String m53(name) => "Nu este ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Vă rugăm să contactați ${familyAdminEmail} pentru a vă schimba codul."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Complexitatea parolei: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Vă rugăm să vorbiți cu asistența ${providerName} dacă ați fost taxat"; - static String m61(endDate) => + static String m62(endDate) => "Perioada de încercare gratuită valabilă până pe ${endDate}.\nUlterior, puteți opta pentru un plan plătit."; - static String m62(toEmail) => + static String m63(toEmail) => "Vă rugăm să ne trimiteți un e-mail la ${toEmail}"; - static String m63(toEmail) => + static String m64(toEmail) => "Vă rugăm să trimiteți jurnalele la \n${toEmail}"; - static String m65(folderName) => "Se procesează ${folderName}..."; + static String m66(folderName) => "Se procesează ${folderName}..."; - static String m66(storeName) => "Evaluați-ne pe ${storeName}"; + static String m67(storeName) => "Evaluați-ne pe ${storeName}"; - static String m68(days, email) => + static String m69(days, email) => "Puteți accesa contul după ${days} zile. O notificare va fi trimisă la ${email}."; - static String m69(email) => + static String m70(email) => "Acum puteți recupera contul ${email} setând o nouă parolă."; - static String m70(email) => "${email} încearcă să vă recupereze contul."; + static String m71(email) => "${email} încearcă să vă recupereze contul."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Amândoi primiți ${storageInGB} GB* gratuit"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} va fi eliminat din acest album distribuit\n\nOrice fotografii adăugate de acesta vor fi, de asemenea, eliminate din album"; - static String m73(endDate) => "Abonamentul se reînnoiește pe ${endDate}"; + static String m74(endDate) => "Abonamentul se reînnoiește pe ${endDate}"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} rezultat găsit', few: '${count} rezultate găsite', other: '${count} de rezultate găsite')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Lungimea secțiunilor nu se potrivesc: ${snapshotLength} != ${searchLength}"; - static String m77(count) => "${count} selectate"; + static String m78(count) => "${count} selectate"; - static String m78(count, yourCount) => + static String m79(count, yourCount) => "${count} selectate (${yourCount} ale dvs.)"; - static String m80(verificationID) => + static String m81(verificationID) => "Acesta este ID-ul meu de verificare: ${verificationID} pentru ente.io."; - static String m81(verificationID) => + static String m82(verificationID) => "Poți confirma că acesta este ID-ul tău de verificare ente.io: ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Codul de recomandare Ente: ${referralCode}\n\nAplică-l în Setări → General → Recomandări pentru a obține ${referralStorageInGB} GB gratuit după ce te înscrii pentru un plan plătit\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Distribuiți cu anumite persoane', one: 'Distribuit cu o persoană', other: 'Distribuit cu ${numberOfPeople} de persoane')}"; - static String m84(emailIDs) => "Distribuit cu ${emailIDs}"; - - static String m85(fileType) => - "Fișierul de tip ${fileType} va fi șters din dispozitivul dvs."; + static String m85(emailIDs) => "Distribuit cu ${emailIDs}"; static String m86(fileType) => - "Fișierul de tip ${fileType} este atât în Ente, cât și în dispozitivul dvs."; + "Fișierul de tip ${fileType} va fi șters din dispozitivul dvs."; static String m87(fileType) => + "Fișierul de tip ${fileType} este atât în Ente, cât și în dispozitivul dvs."; + + static String m88(fileType) => "Fișierul de tip ${fileType} va fi șters din Ente."; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} din ${totalAmount} ${totalStorageUnit} utilizat"; - static String m92(id) => + static String m93(id) => "${id} este deja legat la un alt cont Ente.\nDacă doriți să folosiți ${id} cu acest cont, vă rugăm să contactați asistența noastră"; - static String m93(endDate) => "Abonamentul dvs. va fi anulat pe ${endDate}"; + static String m94(endDate) => "Abonamentul dvs. va fi anulat pe ${endDate}"; - static String m94(completed, total) => + static String m95(completed, total) => "${completed}/${total} amintiri salvate"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "Atingeți pentru a încărca, încărcarea este ignorată în prezent datorită ${ignoreReason}"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "De asemenea, va primii ${storageAmountInGB} GB"; - static String m97(email) => "Acesta este ID-ul de verificare al ${email}"; + static String m98(email) => "Acesta este ID-ul de verificare al ${email}"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Curând', one: 'O zi', other: '${count} de zile')}"; - static String m103(email) => + static String m104(email) => "Ați fost învitat să fiți un contact de moștenire de către ${email}."; - static String m104(galleryType) => + static String m105(galleryType) => "Tipul de galerie ${galleryType} nu este acceptat pentru redenumire"; - static String m105(ignoreReason) => + static String m106(ignoreReason) => "Încărcare ignorată din motivul ${ignoreReason}"; - static String m106(count) => "Se salvează ${count} amintiri..."; + static String m107(count) => "Se salvează ${count} amintiri..."; - static String m107(endDate) => "Valabil până pe ${endDate}"; + static String m108(endDate) => "Valabil până pe ${endDate}"; - static String m108(email) => "Verificare ${email}"; + static String m109(email) => "Verificare ${email}"; - static String m110(email) => "Am trimis un e-mail la ${email}"; + static String m112(email) => "Am trimis un e-mail la ${email}"; - static String m111(count) => + static String m113(count) => "${Intl.plural(count, one: 'acum ${count} an', few: 'acum ${count} ani', other: 'acum ${count} de ani')}"; - static String m113(storageSaved) => "Ați eliberat cu succes ${storageSaved}!"; + static String m115(storageSaved) => "Ați eliberat cu succes ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -771,8 +771,8 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-mail"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("E-mail deja înregistrat."), - "emailChangedTo": m27, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage( "E-mailul nu este înregistrat."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage( @@ -859,7 +859,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Export de date"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("S-au găsit fotografii extra"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Fața nu este încă grupată, vă rugăm să reveniți mai târziu"), "faceRecognition": @@ -910,8 +910,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Tipuri de fișiere"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage( "Tipuri de fișiere și denumiri"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Fișiere șterse"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Fișiere salvate în galerie"), @@ -926,13 +926,13 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("S-au găsit fețe"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Spațiu gratuit revendicat"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Spațiu gratuit utilizabil"), "freeTrial": MessageLookupByLibrary.simpleMessage( "Perioadă de încercare gratuită"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Eliberați spațiu pe dispozitiv"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -944,7 +944,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("General"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Se generează cheile de criptare..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Mergeți la setări"), "googlePlayId": MessageLookupByLibrary.simpleMessage("ID Google Play"), @@ -1028,7 +1028,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Se pare că ceva nu a mers bine. Vă rugăm să încercați din nou după ceva timp. Dacă eroarea persistă, vă rugăm să contactați echipa noastră de asistență."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Articolele afișează numărul de zile rămase până la ștergerea definitivă"), @@ -1060,7 +1060,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Moștenire"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Conturi de moștenire"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Moștenirea permite contactelor de încredere să vă acceseze contul în absența dvs."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1073,7 +1073,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Limită de dispozitive"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Activat"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expirat"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Expirarea linkului"), "linkHasExpired": @@ -1201,7 +1201,7 @@ class MessageLookup extends MessageLookupByLibrary { "moveToAlbum": MessageLookupByLibrary.simpleMessage("Mutare în album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Mutați în albumul ascuns"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("S-a mutat în coșul de gunoi"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1253,10 +1253,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Niciun rezultat"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Nu s-au găsit rezultate"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Nu s-a găsit nicio blocare de sistem"), - "notPersonLabel": m52, + "notPersonLabel": m53, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Nimic distribuit cu dvs. încă"), "nothingToSeeHere": @@ -1266,7 +1266,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Pe dispozitiv"), "onEnte": MessageLookupByLibrary.simpleMessage( "Pe ente"), - "onlyFamilyAdminCanChangeCode": m53, + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Numai el/ea"), "oops": MessageLookupByLibrary.simpleMessage("Ups"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1305,7 +1305,7 @@ class MessageLookup extends MessageLookupByLibrary { "Parola a fost schimbată cu succes"), "passwordLock": MessageLookupByLibrary.simpleMessage("Blocare cu parolă"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Puterea parolei este calculată luând în considerare lungimea parolei, caracterele utilizate și dacă parola apare sau nu în top 10.000 cele mai utilizate parole"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1316,7 +1316,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Plata nu a reușit"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Din păcate, plata dvs. nu a reușit. Vă rugăm să contactați asistență și vom fi bucuroși să vă ajutăm!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Elemente în așteptare"), "pendingSync": @@ -1345,7 +1345,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinAlbum": MessageLookupByLibrary.simpleMessage("Fixați albumul"), "pinLock": MessageLookupByLibrary.simpleMessage("Blocare PIN"), "playOnTv": MessageLookupByLibrary.simpleMessage("Redare album pe TV"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Abonament PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1357,14 +1357,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Vă rugăm să contactați asistența dacă problema persistă"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Vă rugăm să acordați permisiuni"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage( "Vă rugăm, autentificați-vă din nou"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Vă rugăm să selectați linkurile rapide de eliminat"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Vă rugăm să încercați din nou"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1394,7 +1394,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Distribuire privată"), "proceed": MessageLookupByLibrary.simpleMessage("Continuați"), "processed": MessageLookupByLibrary.simpleMessage("Procesate"), - "processingImport": m65, + "processingImport": m66, "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Link public creat"), "publicLinkEnabled": @@ -1406,7 +1406,7 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Evaluați aplicația"), "rateUs": MessageLookupByLibrary.simpleMessage("Evaluați-ne"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "recover": MessageLookupByLibrary.simpleMessage("Recuperare"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recuperare cont"), @@ -1415,7 +1415,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recuperare cont"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Recuperare inițiată"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Cheie de recuperare"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1430,12 +1430,12 @@ class MessageLookup extends MessageLookupByLibrary { "Cheie de recuperare verificată"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Cheia dvs. de recuperare este singura modalitate de a vă recupera fotografiile dacă uitați parola. Puteți găsi cheia dvs. de recuperare în Setări > Cont.\n\nVă rugăm să introduceți aici cheia de recuperare pentru a verifica dacă ați salvat-o corect."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Recuperare reușită!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Un contact de încredere încearcă să vă acceseze contul"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Dispozitivul actual nu este suficient de puternic pentru a vă verifica parola, dar o putem regenera într-un mod care să funcționeze cu toate dispozitivele.\n\nVă rugăm să vă conectați utilizând cheia de recuperare și să vă regenerați parola (dacă doriți, o puteți utiliza din nou pe aceeași)."), "recreatePasswordTitle": @@ -1451,7 +1451,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Dați acest cod prietenilor"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Aceștia se înscriu la un plan cu plată"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Recomandări"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Recomandările sunt momentan întrerupte"), @@ -1483,7 +1483,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Eliminați linkul"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Eliminați participantul"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage( "Eliminați eticheta persoanei"), "removePublicLink": @@ -1504,7 +1504,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Redenumiți fișierul"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Reînnoire abonament"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Raportați o eroare"), "reportBug": MessageLookupByLibrary.simpleMessage("Raportare eroare"), @@ -1585,8 +1585,8 @@ class MessageLookup extends MessageLookupByLibrary { "Invitați persoane și veți vedea aici toate fotografiile distribuite de acestea"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Persoanele vor fi afișate aici odată ce procesarea și sincronizarea este completă"), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Securitate"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Vedeți linkurile albumelor publice în aplicație"), @@ -1621,8 +1621,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Articolele selectate vor fi șterse din toate albumele și mutate în coșul de gunoi."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "send": MessageLookupByLibrary.simpleMessage("Trimitere"), "sendEmail": MessageLookupByLibrary.simpleMessage("Trimiteți e-mail"), "sendInvite": @@ -1655,16 +1655,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Distribuiți un album acum"), "shareLink": MessageLookupByLibrary.simpleMessage("Distribuiți linkul"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Distribuiți numai cu persoanele pe care le doriți"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Descarcă Ente pentru a putea distribui cu ușurință fotografii și videoclipuri în calitate originală\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Distribuiți cu utilizatori din afara Ente"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Distribuiți primul album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1677,7 +1677,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Fotografii partajate noi"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Primiți notificări atunci când cineva adaugă o fotografie la un album distribuit din care faceți parte"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Distribuit mie"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Distribuite cu dvs."), @@ -1693,11 +1693,11 @@ class MessageLookup extends MessageLookupByLibrary { "Deconectați alte dispozitive"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Sunt de acord cu termenii de prestare ai serviciului și politica de confidențialitate"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Acesta va fi șters din toate albumele."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Omiteți"), "social": MessageLookupByLibrary.simpleMessage("Rețele socializare"), "someItemsAreInBothEnteAndYourDevice": @@ -1745,13 +1745,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Spațiu"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Familie"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Dvs."), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Limita de spațiu depășită"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("Puternică"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Abonare"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Aveți nevoie de un abonament plătit activ pentru a activa distribuirea."), @@ -1768,7 +1768,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Sugerați funcționalități"), "support": MessageLookupByLibrary.simpleMessage("Asistență"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronizare oprită"), "syncing": MessageLookupByLibrary.simpleMessage("Sincronizare..."), @@ -1781,7 +1781,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Atingeți pentru a debloca"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Atingeți pentru a încărca"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Se pare că ceva nu a mers bine. Vă rugăm să încercați din nou după ceva timp. Dacă eroarea persistă, vă rugăm să contactați echipa noastră de asistență."), "terminate": MessageLookupByLibrary.simpleMessage("Terminare"), @@ -1804,7 +1804,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Aceste articole vor fi șterse din dispozitivul dvs."), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Acestea vor fi șterse din toate albumele."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1820,7 +1820,7 @@ class MessageLookup extends MessageLookupByLibrary { "Această adresă de e-mail este deja folosită"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Această imagine nu are date exif"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Acesta este ID-ul dvs. de verificare"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1845,11 +1845,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Dimensiune totală"), "trash": MessageLookupByLibrary.simpleMessage("Coș de gunoi"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Decupare"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Contacte de încredere"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Încercați din nou"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Activați copia de rezervă pentru a încărca automat fișierele adăugate la acest dosar de pe dispozitiv în Ente."), @@ -1868,7 +1868,7 @@ class MessageLookup extends MessageLookupByLibrary { "Autentificarea cu doi factori a fost resetată cu succes"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Configurare doi factori"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Dezarhivare"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Dezarhivare album"), @@ -1894,10 +1894,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Se actualizează selecția dosarelor..."), "upgrade": MessageLookupByLibrary.simpleMessage("Îmbunătățire"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Se încarcă fișiere în album..."), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Se salvează o amintire..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1915,7 +1915,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage( "Folosiți fotografia selectată"), "usedSpace": MessageLookupByLibrary.simpleMessage("Spațiu utilizat"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verificare eșuată, încercați din nou"), @@ -1924,7 +1924,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Verificare"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verificare e-mail"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificare"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verificați cheia de acces"), @@ -1962,7 +1962,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Nu se acceptă editarea fotografiilor sau albumelor pe care nu le dețineți încă"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Slabă"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bine ați revenit!"), @@ -1971,7 +1971,7 @@ class MessageLookup extends MessageLookupByLibrary { "Contactul de încredere vă poate ajuta la recuperarea datelor."), "yearShort": MessageLookupByLibrary.simpleMessage("an"), "yearly": MessageLookupByLibrary.simpleMessage("Anual"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Da"), "yesCancel": MessageLookupByLibrary.simpleMessage("Da, anulează"), "yesConvertToViewer": @@ -2003,7 +2003,7 @@ class MessageLookup extends MessageLookupByLibrary { "Nu poți distribui cu tine însuți"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage("Nu aveți articole arhivate."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Contul dvs. a fost șters"), "yourMap": MessageLookupByLibrary.simpleMessage("Harta dvs."), diff --git a/mobile/lib/generated/intl/messages_ru.dart b/mobile/lib/generated/intl/messages_ru.dart index dc27bcc326..e2bb236364 100644 --- a/mobile/lib/generated/intl/messages_ru.dart +++ b/mobile/lib/generated/intl/messages_ru.dart @@ -41,7 +41,7 @@ class MessageLookup extends MessageLookupByLibrary { static String m7(name) => "Любуясь ${name}"; static String m8(count) => - "${Intl.plural(count, zero: 'Нет участников', one: '${count} участник', few: '${count} участника', other: '${count} участников')}"; + "${Intl.plural(count, zero: 'Нет участников', one: '${count} участник', other: '${count} участников')}"; static String m9(versionValue) => "Версия: ${versionValue}"; @@ -67,7 +67,7 @@ class MessageLookup extends MessageLookupByLibrary { static String m15(albumName) => "Совместная ссылка создана для ${albumName}"; static String m16(count) => - "${Intl.plural(count, zero: 'Добавлено 0 соавторов', one: 'Добавлен 1 соавтор', few: 'Добавлено ${count} соавтора', other: 'Добавлено ${count} соавторов')}"; + "${Intl.plural(count, zero: 'Добавлено 0 соавторов', one: 'Добавлен 1 соавтор', other: 'Добавлено ${count} соавторов')}"; static String m17(email, numOfDays) => "Вы собираетесь добавить ${email} в качестве доверенного контакта. Доверенный контакт сможет восстановить ваш аккаунт, если вы будете отсутствовать ${numOfDays} дней."; @@ -81,7 +81,7 @@ class MessageLookup extends MessageLookupByLibrary { static String m20(endpoint) => "Подключено к ${endpoint}"; static String m21(count) => - "${Intl.plural(count, one: 'Удалить ${count} элемент', few: 'Удалить ${count} элемента', other: 'Удалить ${count} элементов')}"; + "${Intl.plural(count, one: 'Удалить ${count} элемент', other: 'Удалить ${count} элементов')}"; static String m22(currentlyDeleting, totalCount) => "Удаление ${currentlyDeleting} / ${totalCount}"; @@ -93,228 +93,228 @@ class MessageLookup extends MessageLookupByLibrary { "Пожалуйста, отправьте письмо на ${supportEmail} с вашего зарегистрированного адреса электронной почты"; static String m25(count, storageSaved) => - "Вы удалили ${Intl.plural(count, one: '${count} дубликат', few: '${count} дубликата', other: '${count} дубликатов')}, освободив (${storageSaved}!)"; + "Вы удалили ${Intl.plural(count, one: '${count} дубликат', other: '${count} дубликатов')}, освободив (${storageSaved}!)"; static String m26(count, formattedSize) => "${count} файлов, по ${formattedSize} каждый"; - static String m27(newEmail) => "Электронная почта изменена на ${newEmail}"; + static String m28(newEmail) => "Электронная почта изменена на ${newEmail}"; - static String m28(email) => "${email} не имеет аккаунта Ente."; + static String m29(email) => "${email} не имеет аккаунта Ente."; - static String m29(email) => + static String m30(email) => "У ${email} нет аккаунта Ente.\n\nОтправьте ему приглашение для обмена фото."; - static String m30(name) => "Обнимая ${name}"; + static String m31(name) => "Обнимая ${name}"; - static String m31(text) => "Дополнительные фото найдены для ${text}"; + static String m32(text) => "Дополнительные фото найдены для ${text}"; - static String m32(name) => "Пир с ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '${formattedNumber} файл на этом устройстве был успешно сохранён', few: '${formattedNumber} файла на этом устройстве были успешно сохранены', other: '${formattedNumber} файлов на этом устройстве были успешно сохранены')}"; + static String m33(name) => "Пир с ${name}"; static String m34(count, formattedNumber) => - "${Intl.plural(count, one: '${formattedNumber} файл в этом альбоме был успешно сохранён', few: '${formattedNumber} файла в этом альбоме были успешно сохранены', other: '${formattedNumber} файлов в этом альбоме были успешно сохранены')}"; + "${Intl.plural(count, one: '${formattedNumber} файл на этом устройстве был успешно сохранён', other: '${formattedNumber} файлов на этом устройстве были успешно сохранены')}"; - static String m35(storageAmountInGB) => + static String m35(count, formattedNumber) => + "${Intl.plural(count, one: '${formattedNumber} файл в этом альбоме был успешно сохранён', other: '${formattedNumber} файлов в этом альбоме были успешно сохранены')}"; + + static String m36(storageAmountInGB) => "${storageAmountInGB} ГБ каждый раз, когда кто-то подписывается на платный тариф и применяет ваш код"; - static String m36(endDate) => + static String m37(endDate) => "Бесплатный пробный период действителен до ${endDate}"; - static String m37(count) => + static String m38(count) => "Вы всё ещё сможете получить доступ к ${Intl.plural(count, one: 'нему', other: 'ним')} в Ente, пока у вас активна подписка"; - static String m38(sizeInMBorGB) => "Освободить ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Освободить ${sizeInMBorGB}"; - static String m39(count, formattedSize) => + static String m40(count, formattedSize) => "${Intl.plural(count, one: 'Его можно удалить с устройства, чтобы освободить ${formattedSize}', other: 'Их можно удалить с устройства, чтобы освободить ${formattedSize}')}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Обработка ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Поход с ${name}"; + static String m42(name) => "Поход с ${name}"; - static String m42(count) => - "${Intl.plural(count, one: '${count} элемент', few: '${count} элемента', other: '${count} элементов')}"; + static String m43(count) => + "${Intl.plural(count, one: '${count} элемент', other: '${count} элементов')}"; - static String m43(name) => "В последний раз с ${name}"; + static String m44(name) => "В последний раз с ${name}"; - static String m44(email) => + static String m45(email) => "${email} пригласил вас стать доверенным контактом"; - static String m45(expiryTime) => "Ссылка истечёт ${expiryTime}"; + static String m46(expiryTime) => "Ссылка истечёт ${expiryTime}"; - static String m46(email) => "Связать человека с ${email}"; + static String m47(email) => "Связать человека с ${email}"; - static String m47(personName, email) => "Это свяжет ${personName} с ${email}"; + static String m48(personName, email) => "Это свяжет ${personName} с ${email}"; - static String m48(count, formattedCount) => - "${Intl.plural(count, zero: 'нет воспоминаний', one: '${formattedCount} воспоминание', few: '${formattedCount} воспоминания', other: '${formattedCount} воспоминаний')}"; + static String m49(count, formattedCount) => + "${Intl.plural(count, zero: 'нет воспоминаний', one: '${formattedCount} воспоминание', other: '${formattedCount} воспоминаний')}"; - static String m49(count) => + static String m50(count) => "${Intl.plural(count, one: 'Переместить элемент', other: 'Переместить элементы')}"; - static String m50(albumName) => "Успешно перемещено в ${albumName}"; + static String m51(albumName) => "Успешно перемещено в ${albumName}"; - static String m51(personName) => "Нет предложений для ${personName}"; + static String m52(personName) => "Нет предложений для ${personName}"; - static String m52(name) => "Не ${name}?"; + static String m53(name) => "Не ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Пожалуйста, свяжитесь с ${familyAdminEmail} для изменения кода."; - static String m54(name) => "Вечеринка с ${name}"; + static String m55(name) => "Вечеринка с ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Надёжность пароля: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Пожалуйста, обратитесь в поддержку ${providerName}, если с вас сняли деньги"; - static String m57(name, age) => "${name} исполнилось ${age}!"; + static String m58(name, age) => "${name} исполнилось ${age}!"; - static String m58(name, age) => "${name} скоро исполнится ${age}"; - - static String m59(count) => - "${Intl.plural(count, zero: 'Нет фото', one: '1 фото', other: '${count} фото')}"; + static String m59(name, age) => "${name} скоро исполнится ${age}"; static String m60(count) => - "${Intl.plural(count, zero: '0 фотографий', one: '1 фотография', few: '${count} фотографии', other: '${count} фотографий')}"; + "${Intl.plural(count, zero: 'Нет фото', one: '1 фото', other: '${count} фото')}"; - static String m61(endDate) => + static String m61(count) => + "${Intl.plural(count, zero: '0 фотографий', one: '1 фотография', other: '${count} фотографий')}"; + + static String m62(endDate) => "Бесплатный пробный период действителен до ${endDate}.\nПосле этого вы можете выбрать платный тариф."; - static String m62(toEmail) => "Пожалуйста, напишите нам на ${toEmail}"; + static String m63(toEmail) => "Пожалуйста, напишите нам на ${toEmail}"; - static String m63(toEmail) => "Пожалуйста, отправьте логи на \n${toEmail}"; + static String m64(toEmail) => "Пожалуйста, отправьте логи на \n${toEmail}"; - static String m64(name) => "Позируя с ${name}"; + static String m65(name) => "Позируя с ${name}"; - static String m65(folderName) => "Обработка ${folderName}..."; + static String m66(folderName) => "Обработка ${folderName}..."; - static String m66(storeName) => "Оцените нас в ${storeName}"; + static String m67(storeName) => "Оцените нас в ${storeName}"; - static String m67(name) => "Вы переназначены на ${name}"; + static String m68(name) => "Вы переназначены на ${name}"; - static String m68(days, email) => + static String m69(days, email) => "Вы сможете получить доступ к аккаунту через ${days} дней. Уведомление будет отправлено на ${email}."; - static String m69(email) => + static String m70(email) => "Теперь вы можете восстановить аккаунт ${email}, установив новый пароль."; - static String m70(email) => "${email} пытается восстановить ваш аккаунт."; + static String m71(email) => "${email} пытается восстановить ваш аккаунт."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Вы оба получаете ${storageInGB} ГБ* бесплатно"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} будет удалён из этого общего альбома\n\nВсе фото, добавленные этим пользователем, также будут удалены из альбома"; - static String m73(endDate) => "Подписка будет продлена ${endDate}"; + static String m74(endDate) => "Подписка будет продлена ${endDate}"; - static String m74(name) => "Путешествие с ${name}"; + static String m75(name) => "Путешествие с ${name}"; - static String m75(count) => - "${Intl.plural(count, one: '${count} результат найден', few: '${count} результата найдено', other: '${count} результатов найдено')}"; + static String m76(count) => + "${Intl.plural(count, one: '${count} результат найден', other: '${count} результатов найдено')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Несоответствие длины разделов: ${snapshotLength} != ${searchLength}"; - static String m77(count) => "${count} выбрано"; + static String m78(count) => "${count} выбрано"; - static String m78(count, yourCount) => + static String m79(count, yourCount) => "${count} выбрано (${yourCount} ваших)"; - static String m79(name) => "Селфи с ${name}"; - - static String m80(verificationID) => - "Вот мой идентификатор подтверждения: ${verificationID} для ente.io."; + static String m80(name) => "Селфи с ${name}"; static String m81(verificationID) => + "Вот мой идентификатор подтверждения: ${verificationID} для ente.io."; + + static String m82(verificationID) => "Привет, можешь подтвердить, что это твой идентификатор подтверждения ente.io: ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Реферальный код Ente: ${referralCode} \n\nПримените его в разделе «Настройки» → «Общие» → «Рефералы», чтобы получить ${referralStorageInGB} ГБ бесплатно после подписки на платный тариф\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Поделиться с конкретными людьми', one: 'Доступно 1 человеку', other: 'Доступно ${numberOfPeople} людям')}"; - static String m84(emailIDs) => "Доступен для ${emailIDs}"; - - static String m85(fileType) => - "Это ${fileType} будет удалено с вашего устройства."; + static String m85(emailIDs) => "Доступен для ${emailIDs}"; static String m86(fileType) => + "Это ${fileType} будет удалено с вашего устройства."; + + static String m87(fileType) => "Это ${fileType} есть и в Ente, и на вашем устройстве."; - static String m87(fileType) => "Это ${fileType} будет удалено из Ente."; + static String m88(fileType) => "Это ${fileType} будет удалено из Ente."; - static String m88(name) => "Спорт с ${name}"; + static String m89(name) => "Спорт с ${name}"; - static String m89(name) => "В центре внимания ${name}"; + static String m90(name) => "В центре внимания ${name}"; - static String m90(storageAmountInGB) => "${storageAmountInGB} ГБ"; + static String m91(storageAmountInGB) => "${storageAmountInGB} ГБ"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "Использовано ${usedAmount} ${usedStorageUnit} из ${totalAmount} ${totalStorageUnit}"; - static String m92(id) => + static String m93(id) => "Ваш ${id} уже связан с другим аккаунтом Ente.\nЕсли вы хотите использовать ${id} с этим аккаунтом, пожалуйста, свяжитесь с нашей службой поддержки"; - static String m93(endDate) => "Ваша подписка будет отменена ${endDate}"; + static String m94(endDate) => "Ваша подписка будет отменена ${endDate}"; - static String m94(completed, total) => + static String m95(completed, total) => "${completed}/${total} воспоминаний сохранено"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "Нажмите для загрузки. Загрузка игнорируется из-за ${ignoreReason}"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "Они тоже получат ${storageAmountInGB} ГБ"; - static String m97(email) => "Это идентификатор подтверждения ${email}"; + static String m98(email) => "Это идентификатор подтверждения ${email}"; - static String m98(count) => - "${Intl.plural(count, one: 'Эта неделя, ${count} год назад', few: 'Эта неделя, ${count} года назад', other: 'Эта неделя, ${count} лет назад')}"; + static String m99(count) => + "${Intl.plural(count, one: 'Эта неделя, ${count} год назад', other: 'Эта неделя, ${count} лет назад')}"; - static String m99(dateFormat) => "${dateFormat} сквозь годы"; + static String m100(dateFormat) => "${dateFormat} сквозь годы"; - static String m100(count) => - "${Intl.plural(count, zero: 'Скоро', one: '1 день', few: '${count} дня', other: '${count} дней')}"; + static String m101(count) => + "${Intl.plural(count, zero: 'Скоро', one: '1 день', other: '${count} дней')}"; - static String m101(year) => "Поездка в ${year}"; + static String m102(year) => "Поездка в ${year}"; - static String m102(location) => "Поездка в ${location}"; + static String m103(location) => "Поездка в ${location}"; - static String m103(email) => + static String m104(email) => "Вы приглашены стать доверенным контактом ${email}."; - static String m104(galleryType) => + static String m105(galleryType) => "Тип галереи ${galleryType} не поддерживает переименование"; - static String m105(ignoreReason) => + static String m106(ignoreReason) => "Загрузка игнорируется из-за ${ignoreReason}"; - static String m106(count) => "Сохранение ${count} воспоминаний..."; + static String m107(count) => "Сохранение ${count} воспоминаний..."; - static String m107(endDate) => "Действительно до ${endDate}"; + static String m108(endDate) => "Действительно до ${endDate}"; - static String m108(email) => "Подтвердить ${email}"; - - static String m109(count) => - "${Intl.plural(count, zero: 'Добавлено 0 зрителей', one: 'Добавлен 1 зритель', other: 'Добавлено ${count} зрителей')}"; - - static String m110(email) => "Мы отправили письмо на ${email}"; + static String m109(email) => "Подтвердить ${email}"; static String m111(count) => + "${Intl.plural(count, zero: 'Добавлено 0 зрителей', one: 'Добавлен 1 зритель', other: 'Добавлено ${count} зрителей')}"; + + static String m112(email) => "Мы отправили письмо на ${email}"; + + static String m113(count) => "${Intl.plural(count, one: '${count} год назад', few: '${count} года назад', other: '${count} лет назад')}"; - static String m112(name) => "Вы и ${name}"; + static String m114(name) => "Вы и ${name}"; - static String m113(storageSaved) => "Вы успешно освободили ${storageSaved}!"; + static String m115(storageSaved) => "Вы успешно освободили ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -543,23 +543,6 @@ class MessageLookup extends MessageLookupByLibrary { "blackFridaySale": MessageLookupByLibrary.simpleMessage( "Распродажа в \"Черную пятницу\""), "blog": MessageLookupByLibrary.simpleMessage("Блог"), - "cLBulkEdit": - MessageLookupByLibrary.simpleMessage("Массовое редактирование дат"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Теперь вы можете выбрать несколько фото и отредактировать дату/время быстро и сразу для всех. Также поддерживается смещение дат."), - "cLFamilyPlan": MessageLookupByLibrary.simpleMessage( - "Ограничения семейного тарифа"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Теперь вы можете установить ограничения на объём хранилища, которое могут использовать члены вашей семьи."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Новая иконка"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Наконец-то новая иконка приложения, которая, как мы считаем, лучше всего отражает нашу работу. Мы также добавили переключатель иконок, чтобы вы могли продолжать использовать старую иконку."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Воспоминания"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Откройте заново свои особенные моменты — в центре внимания ваши любимые люди, поездки и праздники, лучшие снимки и многое другое. Для наилучших впечатлений включите машинное обучение и отметьте себя и своих друзей."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Виджеты"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Теперь доступны виджеты домашнего экрана, интегрированные с воспоминаниями. Они покажут ваши особенные моменты, не открывая приложения."), "cachedData": MessageLookupByLibrary.simpleMessage("Кэшированные данные"), "calculating": MessageLookupByLibrary.simpleMessage("Подсчёт..."), @@ -881,16 +864,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("Электронная почта"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "Электронная почта уже зарегистрирована."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage( "Электронная почта не зарегистрирована."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage( "Подтверждение входа по почте"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( "Отправить логи по электронной почте"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Экстренные контакты"), "empty": MessageLookupByLibrary.simpleMessage("Очистить"), @@ -968,7 +951,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Экспортировать ваши данные"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Найдены дополнительные фото"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Лицо ещё не кластеризовано. Пожалуйста, попробуйте позже"), "faceRecognition": @@ -1006,7 +989,7 @@ class MessageLookup extends MessageLookupByLibrary { "faqs": MessageLookupByLibrary.simpleMessage("Часто задаваемые вопросы"), "favorite": MessageLookupByLibrary.simpleMessage("В избранное"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Обратная связь"), "file": MessageLookupByLibrary.simpleMessage("Файл"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -1020,8 +1003,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Типы файлов"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Типы и названия файлов"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Файлы удалены"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Файлы сохранены в галерею"), @@ -1037,27 +1020,27 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Найденные лица"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Полученное бесплатное хранилище"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Доступное бесплатное хранилище"), "freeTrial": MessageLookupByLibrary.simpleMessage("Бесплатный пробный период"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Освободить место на устройстве"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Освободите место на устройстве, удалив файлы, которые уже сохранены в резервной копии."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Освободить место"), - "freeUpSpaceSaving": m39, + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Галерея"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "В галерее отображается до 1000 воспоминаний"), "general": MessageLookupByLibrary.simpleMessage("Общие"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Генерация ключей шифрования..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Перейти в настройки"), "googlePlayId": @@ -1088,7 +1071,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Скрыть общие элементы из основной галереи"), "hiding": MessageLookupByLibrary.simpleMessage("Скрытие..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("Размещено на OSM France"), "howItWorks": MessageLookupByLibrary.simpleMessage("Как это работает"), @@ -1146,7 +1129,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Похоже, что-то пошло не так. Пожалуйста, повторите попытку через некоторое время. Если ошибка сохраняется, обратитесь в нашу службу поддержки."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "На элементах отображается количество дней, оставшихся до их безвозвратного удаления"), @@ -1168,7 +1151,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Пожалуйста, помогите нам с этой информацией"), "language": MessageLookupByLibrary.simpleMessage("Язык"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Последнее обновление"), "lastYearsTrip": @@ -1182,7 +1165,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Наследие"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Наследуемые аккаунты"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Наследие позволяет доверенным контактам получить доступ к вашему аккаунту в ваше отсутствие."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1200,7 +1183,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("чтобы быстрее делиться"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Включена"), "linkExpired": MessageLookupByLibrary.simpleMessage("Истекла"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Срок действия ссылки"), "linkHasExpired": @@ -1209,8 +1192,8 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Связать человека"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage("чтобы было удобнее делиться"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Живые фото"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Вы можете поделиться подпиской с вашей семьёй"), @@ -1299,7 +1282,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Я"), - "memoryCount": m48, + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Мерч"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Объединить с существующим"), @@ -1332,14 +1315,14 @@ class MessageLookup extends MessageLookupByLibrary { "mostRelevant": MessageLookupByLibrary.simpleMessage("Самые актуальные"), "mountains": MessageLookupByLibrary.simpleMessage("За холмами"), - "moveItem": m49, + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Переместите выбранные фото на одну дату"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Переместить в альбом"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Переместить в скрытый альбом"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Перемещено в корзину"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1394,10 +1377,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Нет результатов"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Нет результатов"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Системная блокировка не найдена"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("Не этот человек?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( @@ -1410,7 +1393,7 @@ class MessageLookup extends MessageLookupByLibrary { "onEnte": MessageLookupByLibrary.simpleMessage("В ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("Снова в пути"), - "onlyFamilyAdminCanChangeCode": m53, + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Только он(а)"), "oops": MessageLookupByLibrary.simpleMessage("Ой"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1441,7 +1424,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("Подключение завершено"), "panorama": MessageLookupByLibrary.simpleMessage("Панорама"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage("Проверка всё ещё ожидается"), "passkey": MessageLookupByLibrary.simpleMessage("Ключ доступа"), @@ -1451,7 +1434,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("Пароль успешно изменён"), "passwordLock": MessageLookupByLibrary.simpleMessage("Защита паролем"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Надёжность пароля определяется его длиной, используемыми символами и присутствием среди 10000 самых популярных паролей"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1462,7 +1445,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Платёж не удался"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "К сожалению, ваш платёж не удался. Пожалуйста, свяжитесь с поддержкой, и мы вам поможем!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Элементы в очереди"), "pendingSync": @@ -1476,21 +1459,21 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Удалить безвозвратно"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "Удалить с устройства безвозвратно?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Имя человека"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Пушистые спутники"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Описания фото"), "photoGridSize": MessageLookupByLibrary.simpleMessage("Размер сетки фото"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("фото"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Фото"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Добавленные вами фото будут удалены из альбома"), - "photosCount": m60, + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "Фото сохранят относительную разницу во времени"), @@ -1502,7 +1485,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Воспроизвести альбом на ТВ"), "playOriginal": MessageLookupByLibrary.simpleMessage("Воспроизвести оригинал"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Воспроизвести поток"), "playstoreSubscription": @@ -1516,14 +1499,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Пожалуйста, обратитесь в поддержку, если проблема сохраняется"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Пожалуйста, предоставьте разрешения"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Пожалуйста, войдите снова"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Пожалуйста, выберите быстрые ссылки для удаления"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Пожалуйста, попробуйте снова"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1538,7 +1521,7 @@ class MessageLookup extends MessageLookupByLibrary { "Пожалуйста, подождите некоторое время перед повторной попыткой"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Пожалуйста, подождите, это займёт некоторое время."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("Подготовка логов..."), "preserveMore": @@ -1558,7 +1541,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Продолжить"), "processed": MessageLookupByLibrary.simpleMessage("Обработано"), "processing": MessageLookupByLibrary.simpleMessage("Обработка"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Обработка видео"), "publicLinkCreated": @@ -1572,10 +1555,10 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Оценить приложение"), "rateUs": MessageLookupByLibrary.simpleMessage("Оцените нас"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("Переназначить \"Меня\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Переназначение..."), "recover": MessageLookupByLibrary.simpleMessage("Восстановить"), @@ -1586,7 +1569,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Восстановить аккаунт"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Восстановление начато"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Ключ восстановления"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1601,12 +1584,12 @@ class MessageLookup extends MessageLookupByLibrary { "Ключ восстановления подтверждён"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Ваш ключ восстановления — единственный способ восстановить ваши фото, если вы забудете пароль. Вы можете найти ключ восстановления в разделе «Настройки» → «Аккаунт».\n\nПожалуйста, введите ваш ключ восстановления здесь, чтобы убедиться, что вы сохранили его правильно."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Успешное восстановление!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Доверенный контакт пытается получить доступ к вашему аккаунту"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Текущее устройство недостаточно мощное для проверки вашего пароля, но мы можем сгенерировать его снова так, чтобы он работал на всех устройствах.\n\nПожалуйста, войдите, используя ваш ключ восстановления, и сгенерируйте пароль (при желании вы можете использовать тот же самый)."), "recreatePasswordTitle": @@ -1622,7 +1605,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Даёте этот код своим друзьям"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Они подписываются на платный тариф"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Рефералы"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Реферальная программа временно приостановлена"), @@ -1654,7 +1637,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Удалить ссылку"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Удалить участника"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Удалить метку человека"), "removePublicLink": @@ -1676,7 +1659,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Переименовать файл"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Продлить подписку"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Сообщить об ошибке"), "reportBug": MessageLookupByLibrary.simpleMessage("Сообщить об ошибке"), @@ -1703,7 +1686,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Посмотреть предложения"), "right": MessageLookupByLibrary.simpleMessage("Вправо"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Повернуть"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Повернуть влево"), "rotateRight": MessageLookupByLibrary.simpleMessage("Повернуть вправо"), @@ -1760,8 +1743,8 @@ class MessageLookup extends MessageLookupByLibrary { "Приглашайте людей, и здесь появятся все фото, которыми они поделились"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Люди появятся здесь после завершения обработки и синхронизации"), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Безопасность"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Просматривать публичные ссылки на альбомы в приложении"), @@ -1810,9 +1793,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Выбранные элементы будут отвязаны от этого человека, но не удалены из вашей библиотеки."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, - "selfiesWithThem": m79, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Отправить"), "sendEmail": MessageLookupByLibrary.simpleMessage( "Отправить электронное письмо"), @@ -1847,16 +1830,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Поделиться альбомом"), "shareLink": MessageLookupByLibrary.simpleMessage("Поделиться ссылкой"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Делитесь только с теми, с кем хотите"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Скачай Ente, чтобы мы могли легко делиться фото и видео в оригинальном качестве\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Поделиться с пользователями, не использующими Ente"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Поделитесь своим первым альбомом"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1867,7 +1850,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Новые общие фото"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Получать уведомления, когда кто-то добавляет фото в общий альбом, в котором вы состоите"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Со мной поделились"), "sharedWithYou": @@ -1886,11 +1869,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Выйти с других устройств"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Я согласен с условиями предоставления услуг и политикой конфиденциальности"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Оно будет удалено из всех альбомов."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Пропустить"), "social": MessageLookupByLibrary.simpleMessage("Социальные сети"), "someItemsAreInBothEnteAndYourDevice": MessageLookupByLibrary.simpleMessage( @@ -1925,8 +1908,8 @@ class MessageLookup extends MessageLookupByLibrary { "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Сначала старые"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Успех"), - "sportsWithThem": m88, - "spotlightOnThem": m89, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Вы в центре внимания"), "startAccountRecoveryTitle": @@ -1941,15 +1924,15 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Хранилище"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Семья"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Вы"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Превышен лимит хранилища"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Информация о потоке"), "strongStrength": MessageLookupByLibrary.simpleMessage("Высокая"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Подписаться"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Вам нужна активная платная подписка, чтобы включить общий доступ."), @@ -1967,7 +1950,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Предложить идею"), "sunrise": MessageLookupByLibrary.simpleMessage("На горизонте"), "support": MessageLookupByLibrary.simpleMessage("Поддержка"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Синхронизация остановлена"), "syncing": MessageLookupByLibrary.simpleMessage("Синхронизация..."), @@ -1980,7 +1963,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Нажмите для разблокировки"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Нажмите для загрузки"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Похоже, что-то пошло не так. Пожалуйста, повторите попытку через некоторое время. Если ошибка сохраняется, обратитесь в нашу службу поддержки."), "terminate": MessageLookupByLibrary.simpleMessage("Завершить"), @@ -2004,7 +1987,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Эти элементы будут удалены с вашего устройства."), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Они будут удалены из всех альбомов."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -2021,12 +2004,12 @@ class MessageLookup extends MessageLookupByLibrary { "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Это фото не имеет данных EXIF"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Это я!"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Это ваш идентификатор подтверждения"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("Эта неделя сквозь годы"), - "thisWeekXYearsAgo": m98, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Это завершит ваш сеанс на следующем устройстве:"), @@ -2038,7 +2021,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Это удалит публичные ссылки всех выбранных быстрых ссылок."), - "throughTheYears": m99, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "Для блокировки приложения, пожалуйста, настройте код или экран блокировки в настройках устройства."), @@ -2052,13 +2035,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("всего"), "totalSize": MessageLookupByLibrary.simpleMessage("Общий размер"), "trash": MessageLookupByLibrary.simpleMessage("Корзина"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Сократить"), - "tripInYear": m101, - "tripToLocation": m102, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Доверенные контакты"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Попробовать снова"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Включите резервное копирование, чтобы автоматически загружать файлы из этой папки на устройстве в Ente."), @@ -2078,7 +2061,7 @@ class MessageLookup extends MessageLookupByLibrary { "Двухфакторная аутентификация успешно сброшена"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Настройка двухфакторной аутентификации"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Извлечь из архива"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Извлечь альбом из архива"), @@ -2101,10 +2084,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("Обновление выбора папок..."), "upgrade": MessageLookupByLibrary.simpleMessage("Улучшить"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage("Загрузка файлов в альбом..."), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage( "Сохранение 1 воспоминания..."), "upto50OffUntil4thDec": @@ -2123,7 +2106,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Использовать выбранное фото"), "usedSpace": MessageLookupByLibrary.simpleMessage("Использовано места"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Проверка не удалась, пожалуйста, попробуйте снова"), @@ -2132,7 +2115,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Подтвердить"), "verifyEmail": MessageLookupByLibrary.simpleMessage( "Подтвердить электронную почту"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Подтвердить"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Подтвердить ключ доступа"), @@ -2143,8 +2126,6 @@ class MessageLookup extends MessageLookupByLibrary { "Проверка ключа восстановления..."), "videoInfo": MessageLookupByLibrary.simpleMessage("Информация о видео"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("видео"), - "videoStreaming": - MessageLookupByLibrary.simpleMessage("Потоковое видео"), "videos": MessageLookupByLibrary.simpleMessage("Видео"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Просмотр активных сессий"), @@ -2160,7 +2141,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Увидеть ключ восстановления"), "viewer": MessageLookupByLibrary.simpleMessage("Зритель"), - "viewersSuccessfullyAdded": m109, + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Пожалуйста, посетите web.ente.io для управления вашей подпиской"), "waitingForVerification": @@ -2173,7 +2154,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Мы не поддерживаем редактирование фото и альбомов, которые вам пока не принадлежат"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Низкая"), "welcomeBack": MessageLookupByLibrary.simpleMessage("С возвращением!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Что нового"), @@ -2181,7 +2162,7 @@ class MessageLookup extends MessageLookupByLibrary { "Доверенный контакт может помочь в восстановлении ваших данных."), "yearShort": MessageLookupByLibrary.simpleMessage("год"), "yearly": MessageLookupByLibrary.simpleMessage("Ежегодно"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Да"), "yesCancel": MessageLookupByLibrary.simpleMessage("Да, отменить"), "yesConvertToViewer": @@ -2195,7 +2176,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage( "Да, сбросить данные человека"), "you": MessageLookupByLibrary.simpleMessage("Вы"), - "youAndThem": m112, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("Вы на семейном тарифе!"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage( @@ -2214,7 +2195,7 @@ class MessageLookup extends MessageLookupByLibrary { "Вы не можете поделиться с самим собой"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "У вас нет архивных элементов."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Ваш аккаунт был удалён"), "yourMap": MessageLookupByLibrary.simpleMessage("Ваша карта"), diff --git a/mobile/lib/generated/intl/messages_sv.dart b/mobile/lib/generated/intl/messages_sv.dart index b31857bc18..4ba15c21eb 100644 --- a/mobile/lib/generated/intl/messages_sv.dart +++ b/mobile/lib/generated/intl/messages_sv.dart @@ -47,62 +47,62 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} filer, ${formattedSize} vardera"; - static String m29(email) => + static String m30(email) => "${email} har inte ett Ente-konto.\n\nSkicka dem en inbjudan för att dela bilder."; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB varje gång någon registrerar sig för en betalplan och tillämpar din kod"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} objekt', other: '${count} objekt')}"; - static String m45(expiryTime) => "Länken upphör att gälla ${expiryTime}"; + static String m46(expiryTime) => "Länken upphör att gälla ${expiryTime}"; - static String m52(name) => "Inte ${name}?"; + static String m53(name) => "Inte ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Kontakta ${familyAdminEmail} för att ändra din kod."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Lösenordsstyrka: ${passwordStrengthValue}"; - static String m66(storeName) => "Betygsätt oss på ${storeName}"; + static String m67(storeName) => "Betygsätt oss på ${storeName}"; - static String m71(storageInGB) => "3. Ni får båda ${storageInGB} GB* gratis"; + static String m72(storageInGB) => "3. Ni får båda ${storageInGB} GB* gratis"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} kommer att tas bort från detta delade album\n\nAlla bilder som lagts till av dem kommer också att tas bort från albumet"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} resultat hittades', other: '${count} resultat hittades')}"; - static String m80(verificationID) => + static String m81(verificationID) => "Här är mitt verifierings-ID: ${verificationID} för ente.io."; - static String m81(verificationID) => + static String m82(verificationID) => "Hallå, kan du bekräfta att detta är ditt ente.io verifierings-ID: ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Ente värvningskod: ${referralCode} \n\nTillämpa den i Inställningar → Allmänt → Hänvisningar för att få ${referralStorageInGB} GB gratis när du registrerar dig för en betalplan\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Dela med specifika personer', one: 'Delad med en person', other: 'Delad med ${numberOfPeople} personer')}"; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "De får också ${storageAmountInGB} GB"; - static String m97(email) => "Detta är ${email}s verifierings-ID"; + static String m98(email) => "Detta är ${email}s verifierings-ID"; - static String m106(count) => "Bevarar ${count} minnen..."; + static String m107(count) => "Bevarar ${count} minnen..."; - static String m108(email) => "Bekräfta ${email}"; + static String m109(email) => "Bekräfta ${email}"; - static String m110(email) => + static String m112(email) => "Vi har skickat ett e-postmeddelande till ${email}"; - static String m111(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} år sedan', other: '${count} år sedan')}"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -292,7 +292,7 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-post"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "E-postadress redan registrerad."), - "emailNoEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage( "E-postadressen är inte registrerad."), "encryption": MessageLookupByLibrary.simpleMessage("Kryptering"), @@ -342,7 +342,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Glömt lösenord"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Gratis lagring begärd"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Gratis lagringsutrymme som kan användas"), "freeTrial": MessageLookupByLibrary.simpleMessage("Gratis provperiod"), @@ -379,7 +379,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bjud in dina vänner"), "inviteYourFriendsToEnte": MessageLookupByLibrary.simpleMessage( "Bjud in dina vänner till Ente"), - "itemCount": m42, + "itemCount": m43, "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Valda objekt kommer att tas bort från detta album"), "keepPhotos": MessageLookupByLibrary.simpleMessage("Behåll foton"), @@ -392,7 +392,7 @@ class MessageLookup extends MessageLookupByLibrary { "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Enhetsgräns"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktiverat"), "linkExpired": MessageLookupByLibrary.simpleMessage("Upphört"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Länken upphör"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Länk har upphört att gälla"), @@ -440,9 +440,9 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Inga resultat"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Inga resultat hittades"), - "notPersonLabel": m52, + "notPersonLabel": m53, "ok": MessageLookupByLibrary.simpleMessage("OK"), - "onlyFamilyAdminCanChangeCode": m53, + "onlyFamilyAdminCanChangeCode": m54, "oops": MessageLookupByLibrary.simpleMessage("Hoppsan"), "oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage("Oj, något gick fel"), @@ -453,7 +453,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("Lösenordet har ändrats"), "passwordLock": MessageLookupByLibrary.simpleMessage("Lösenordskydd"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordWarning": MessageLookupByLibrary.simpleMessage( "Vi lagrar inte detta lösenord, så om du glömmer bort det, kan vi inte dekryptera dina data"), "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage( @@ -470,7 +470,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Integritetspolicy"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage("Offentlig länk aktiverad"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "recover": MessageLookupByLibrary.simpleMessage("Återställ"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Återställ konto"), @@ -500,7 +500,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Ge denna kod till dina vänner"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. De registrerar sig för en betalplan"), - "referralStep3": m71, + "referralStep3": m72, "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Hänvisningar är för närvarande pausade"), "remove": MessageLookupByLibrary.simpleMessage("Ta bort"), @@ -511,7 +511,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Radera länk"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Ta bort användaren"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( "Några av de objekt som du tar bort lades av andra personer, och du kommer att förlora tillgång till dem"), "removeWithQuestionMark": @@ -544,7 +544,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Albumnamn"), "searchFileTypesAndNamesEmptySection": MessageLookupByLibrary.simpleMessage("Filtyper och namn"), - "searchResultCount": m75, + "searchResultCount": m76, "selectAlbum": MessageLookupByLibrary.simpleMessage("Välj album"), "selectAll": MessageLookupByLibrary.simpleMessage("Markera allt"), "selectFoldersForBackup": MessageLookupByLibrary.simpleMessage( @@ -567,14 +567,14 @@ class MessageLookup extends MessageLookupByLibrary { "share": MessageLookupByLibrary.simpleMessage("Dela"), "shareALink": MessageLookupByLibrary.simpleMessage("Dela en länk"), "shareLink": MessageLookupByLibrary.simpleMessage("Dela länk"), - "shareMyVerificationID": m80, - "shareTextConfirmOthersVerificationID": m81, + "shareMyVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Ladda ner Ente så att vi enkelt kan dela bilder och videor med originell kvalitet\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Dela med icke-Ente användare"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Dela ditt första album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -605,7 +605,7 @@ class MessageLookup extends MessageLookupByLibrary { "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Sortera efter"), "status": MessageLookupByLibrary.simpleMessage("Status"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Du"), - "storageInGB": m90, + "storageInGB": m91, "strongStrength": MessageLookupByLibrary.simpleMessage("Starkt"), "subscribe": MessageLookupByLibrary.simpleMessage("Prenumerera"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( @@ -625,12 +625,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Återställningsnyckeln du angav är felaktig"), "theme": MessageLookupByLibrary.simpleMessage("Tema"), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "thisCanBeUsedToRecoverYourAccountIfYou": MessageLookupByLibrary.simpleMessage( "Detta kan användas för att återställa ditt konto om du förlorar din andra faktor"), "thisDevice": MessageLookupByLibrary.simpleMessage("Den här enheten"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Detta är ditt verifierings-ID"), "thisWillLogYouOutOfTheFollowingDevice": @@ -658,7 +658,7 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("Uppdaterar mappval..."), "upgrade": MessageLookupByLibrary.simpleMessage("Uppgradera"), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Bevarar 1 minne..."), "usableReferralStorageInfo": MessageLookupByLibrary.simpleMessage( @@ -671,7 +671,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Bekräfta"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Bekräfta e-postadress"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verifiera nyckel"), "verifyPassword": @@ -687,12 +687,12 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Visa återställningsnyckel"), "viewer": MessageLookupByLibrary.simpleMessage("Bildvy"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Svagt"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Välkommen tillbaka!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Nyheter"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Ja"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ja, avbryt"), "yesConvertToViewer": diff --git a/mobile/lib/generated/intl/messages_ta.dart b/mobile/lib/generated/intl/messages_ta.dart index 30c00c6d72..2255c71ef9 100644 --- a/mobile/lib/generated/intl/messages_ta.dart +++ b/mobile/lib/generated/intl/messages_ta.dart @@ -39,6 +39,10 @@ class MessageLookup extends MessageLookupByLibrary { "deleteReason1": MessageLookupByLibrary.simpleMessage( "எனக்கு தேவையான ஒரு முக்கிய அம்சம் இதில் இல்லை"), "email": MessageLookupByLibrary.simpleMessage("மின்னஞ்சல்"), + "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( + "மின்னஞ்சல் முன்பே பதிவுசெய்யப்பட்டுள்ளது."), + "emailNotRegistered": MessageLookupByLibrary.simpleMessage( + "மின்னஞ்சல் பதிவு செய்யப்படவில்லை."), "enterValidEmail": MessageLookupByLibrary.simpleMessage( "சரியான மின்னஞ்சல் முகவரியை உள்ளிடவும்."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage( @@ -48,6 +52,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("தவறான மின்னஞ்சல் முகவரி"), "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "இந்த தகவலுடன் தயவுசெய்து எங்களுக்கு உதவுங்கள்"), - "verify": MessageLookupByLibrary.simpleMessage("சரிபார்க்கவும்") + "selectReason": MessageLookupByLibrary.simpleMessage( + "காரணத்தைத் தேர்ந்தெடுக்கவும்"), + "verify": MessageLookupByLibrary.simpleMessage("சரிபார்க்கவும்"), + "yourAccountHasBeenDeleted": + MessageLookupByLibrary.simpleMessage("உங்கள் கணக்கு நீக்கப்பட்டது") }; } diff --git a/mobile/lib/generated/intl/messages_th.dart b/mobile/lib/generated/intl/messages_th.dart index be3154dd7e..c977129b0e 100644 --- a/mobile/lib/generated/intl/messages_th.dart +++ b/mobile/lib/generated/intl/messages_th.dart @@ -31,19 +31,19 @@ class MessageLookup extends MessageLookupByLibrary { static String m24(supportEmail) => "กรุณาส่งอีเมลไปที่ ${supportEmail} จากที่อยู่อีเมลที่คุณลงทะเบียนไว้"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "กำลังประมวลผล ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => "${Intl.plural(count, other: '${count} รายการ')}"; + static String m43(count) => "${Intl.plural(count, other: '${count} รายการ')}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "ความแข็งแรงของรหัสผ่าน: ${passwordStrengthValue}"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "ใช้ไป ${usedAmount} ${usedStorageUnit} จาก ${totalAmount} ${totalStorageUnit}"; - static String m110(email) => "เราได้ส่งจดหมายไปยัง ${email}"; + static String m112(email) => "เราได้ส่งจดหมายไปยัง ${email}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -167,7 +167,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("เพิ่มคำอธิบาย..."), "forgotPassword": MessageLookupByLibrary.simpleMessage("ลืมรหัสผ่าน"), "freeTrial": MessageLookupByLibrary.simpleMessage("ทดลองใช้ฟรี"), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("ไปที่การตั้งค่า"), "hide": MessageLookupByLibrary.simpleMessage("ซ่อน"), "hostedAtOsmFrance": @@ -190,7 +190,7 @@ class MessageLookup extends MessageLookupByLibrary { "invalidKey": MessageLookupByLibrary.simpleMessage("รหัสไม่ถูกต้อง"), "invalidRecoveryKey": MessageLookupByLibrary.simpleMessage( "คีย์การกู้คืนที่คุณป้อนไม่ถูกต้อง โปรดตรวจสอบให้แน่ใจว่ามี 24 คำ และตรวจสอบการสะกดของแต่ละคำ\n\nหากคุณป้อนรหัสกู้คืนที่เก่ากว่า ตรวจสอบให้แน่ใจว่ามีความยาว 64 ตัวอักษร และตรวจสอบแต่ละตัวอักษร"), - "itemCount": m42, + "itemCount": m43, "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage("กรุณาช่วยเราด้วยข้อมูลนี้"), "lastUpdated": MessageLookupByLibrary.simpleMessage("อัปเดตล่าสุด"), @@ -228,7 +228,7 @@ class MessageLookup extends MessageLookupByLibrary { "password": MessageLookupByLibrary.simpleMessage("รหัสผ่าน"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("เปลี่ยนรหัสผ่านสำเร็จ"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordWarning": MessageLookupByLibrary.simpleMessage( "เราไม่จัดเก็บรหัสผ่านนี้ ดังนั้นหากคุณลืม เราจะไม่สามารถถอดรหัสข้อมูลของคุณ"), "peopleUsingYourCode": @@ -300,7 +300,7 @@ class MessageLookup extends MessageLookupByLibrary { "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("ครอบครัว"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("คุณ"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("แข็งแรง"), "syncStopped": MessageLookupByLibrary.simpleMessage("หยุดการซิงค์แล้ว"), "syncing": MessageLookupByLibrary.simpleMessage("กำลังซิงค์..."), @@ -341,7 +341,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ดูคีย์การกู้คืน"), "waitingForWifi": MessageLookupByLibrary.simpleMessage("กำลังรอ WiFi..."), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("อ่อน"), "welcomeBack": MessageLookupByLibrary.simpleMessage("ยินดีต้อนรับกลับมา!"), diff --git a/mobile/lib/generated/intl/messages_tr.dart b/mobile/lib/generated/intl/messages_tr.dart index ad37a2c98e..8859ab5212 100644 --- a/mobile/lib/generated/intl/messages_tr.dart +++ b/mobile/lib/generated/intl/messages_tr.dart @@ -98,224 +98,229 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} dosyalar, ${formattedSize} her biri"; - static String m27(newEmail) => "E-posta ${newEmail} olarak değiştirildi"; + static String m27(name) => "Bu e-posta zaten ${name} kişisine bağlı."; - static String m28(email) => "${email} bir Ente hesabına sahip değil"; + static String m28(newEmail) => "E-posta ${newEmail} olarak değiştirildi"; - static String m29(email) => + static String m29(email) => "${email} bir Ente hesabına sahip değil"; + + static String m30(email) => "${email}, Ente hesabı bulunmamaktadır.\n\nOnlarla fotoğraf paylaşımı için bir davet gönder."; - static String m30(name) => "${name}\'e sarılmak"; + static String m31(name) => "${name}\'e sarılmak"; - static String m31(text) => "${text} için ekstra fotoğraflar bulundu"; + static String m32(text) => "${text} için ekstra fotoğraflar bulundu"; - static String m32(name) => "${name} ile ziyafet"; - - static String m33(count, formattedNumber) => - "Bu cihazdaki ${Intl.plural(count, one: '1 file', other: '${formattedNumber} dosya')} güvenli bir şekilde yedeklendi"; + static String m33(name) => "${name} ile ziyafet"; static String m34(count, formattedNumber) => + "Bu cihazdaki ${Intl.plural(count, one: '1 file', other: '${formattedNumber} dosya')} güvenli bir şekilde yedeklendi"; + + static String m35(count, formattedNumber) => "Bu albümdeki ${Intl.plural(count, one: '1 file', other: '${formattedNumber} dosya')} güvenli bir şekilde yedeklendi"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "Birisinin davet kodunuzu uygulayıp ücretli hesap açtığı her seferede ${storageAmountInGB} GB"; - static String m36(endDate) => "Ücretsiz deneme ${endDate} sona erir"; + static String m37(endDate) => "Ücretsiz deneme ${endDate} sona erir"; - static String m37(count) => + static String m38(count) => "Aktif bir aboneliğiniz olduğu sürece Ente üzerinden ${Intl.plural(count, one: 'ona', other: 'onlara')} hâlâ erişebilirsiniz"; - static String m38(sizeInMBorGB) => "${sizeInMBorGB} yer açın"; + static String m39(sizeInMBorGB) => "${sizeInMBorGB} yer açın"; - static String m39(count, formattedSize) => + static String m40(count, formattedSize) => "${Intl.plural(count, one: 'Cihazdan silinerek ${formattedSize} boşaltılabilir', other: 'Cihazdan silinerek ${formattedSize} boşaltılabilirler')}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Siliniyor ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "${name} ile doğa yürüyüşü"; + static String m42(name) => "${name} ile doğa yürüyüşü"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} öğe', other: '${count} öğeler')}"; - static String m43(name) => "${name} ile son an"; + static String m44(name) => "${name} ile son an"; - static String m44(email) => + static String m45(email) => "${email} sizi güvenilir bir kişi olmaya davet etti"; - static String m45(expiryTime) => - "Bu bağlantı ${expiryTime} dan sonra geçersiz olacaktır"; + static String m46(expiryTime) => + "Bu bağlantı ${expiryTime} tarihinden itibaren geçersiz olacaktır"; - static String m46(email) => "Kişiyi ${email} adresine bağlayın"; + static String m47(email) => "Kişiyi ${email} adresine bağlayın"; - static String m47(personName, email) => + static String m48(personName, email) => "Bu, ${personName} ile ${email} arasında bağlantı kuracaktır."; - static String m48(count, formattedCount) => + static String m49(count, formattedCount) => "${Intl.plural(count, zero: 'hiç anı yok', one: '${formattedCount} anı', other: '${formattedCount} anı')}"; - static String m49(count) => + static String m50(count) => "${Intl.plural(count, one: 'Öğeyi taşı', other: 'Öğeleri taşı')}"; - static String m50(albumName) => "${albumName} adlı albüme başarıyla taşındı"; + static String m51(albumName) => "${albumName} adlı albüme başarıyla taşındı"; - static String m51(personName) => "${personName} için öneri yok"; + static String m52(personName) => "${personName} için öneri yok"; - static String m52(name) => "${name} değil mi?"; + static String m53(name) => "${name} değil mi?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Kodunuzu değiştirmek için lütfen ${familyAdminEmail} ile iletişime geçin."; - static String m54(name) => "${name} ile parti"; + static String m55(name) => "${name} ile parti"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Şifrenin güçlülük seviyesi: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Sizden ücret alındıysa lütfen ${providerName} destek ekibiyle görüşün"; - static String m57(name, age) => "${name} ${age} yaşında!"; + static String m58(name, age) => "${name} ${age} yaşında!"; - static String m58(name, age) => "${name} yakında ${age} yaşına giriyor"; - - static String m59(count) => - "${Intl.plural(count, zero: 'Fotoğraf yok', one: '1 fotoğraf', other: '${count} fotoğraf')}"; + static String m59(name, age) => "${name} yakında ${age} yaşına giriyor"; static String m60(count) => + "${Intl.plural(count, zero: 'Fotoğraf yok', one: '1 fotoğraf', other: '${count} fotoğraf')}"; + + static String m61(count) => "${Intl.plural(count, zero: '0 fotoğraf', one: '1 fotoğraf', other: '${count} fotoğraf')}"; - static String m61(endDate) => + static String m62(endDate) => "Ücretsiz deneme süresi ${endDate} tarihine kadar geçerlidir.\nDaha sonra ücretli bir plan seçebilirsiniz."; - static String m62(toEmail) => "Lütfen bize ${toEmail} adresinden ulaşın"; + static String m63(toEmail) => "Lütfen bize ${toEmail} adresinden ulaşın"; - static String m63(toEmail) => + static String m64(toEmail) => "Lütfen kayıtları şu adrese gönderin\n${toEmail}"; - static String m64(name) => "${name} ile poz verme"; + static String m65(name) => "${name} ile poz verme"; - static String m65(folderName) => "İşleniyor ${folderName}..."; + static String m66(folderName) => "İşleniyor ${folderName}..."; - static String m66(storeName) => "Bizi ${storeName} üzerinden değerlendirin"; + static String m67(storeName) => "Bizi ${storeName} üzerinden değerlendirin"; - static String m67(name) => "Sizi ${name}\'e yeniden atadı"; + static String m68(name) => "Sizi ${name}\'e yeniden atadı"; - static String m68(days, email) => + static String m69(days, email) => "Hesabınıza ${days} gün sonra erişebilirsiniz. ${email} adresine bir bildirim gönderilecektir."; - static String m69(email) => + static String m70(email) => "Artık yeni bir parola belirleyerek ${email} hesabını kurtarabilirsiniz."; - static String m70(email) => "${email} hesabınızı kurtarmaya çalışıyor."; + static String m71(email) => "${email} hesabınızı kurtarmaya çalışıyor."; - static String m71(storageInGB) => "3. İkinizde bedava ${storageInGB} GB alın"; + static String m72(storageInGB) => "3. İkinizde bedava ${storageInGB} GB alın"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} bu paylaşılan albümden kaldırılacaktır\n\nOnlar tarafından eklenen tüm fotoğraflar da albümden kaldırılacaktır"; - static String m73(endDate) => "Abonelik ${endDate} tarihinde yenilenir"; + static String m74(endDate) => "Abonelik ${endDate} tarihinde yenilenir"; - static String m74(name) => "${name} ile yolculuk"; + static String m75(name) => "${name} ile yolculuk"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} yıl önce', other: '${count} yıl önce')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Bölüm uzunluğu uyuşmazlığı: ${snapshotLength} != ${searchLength}"; - static String m77(count) => "${count} seçildi"; + static String m78(count) => "${count} seçildi"; - static String m78(count, yourCount) => + static String m79(count, yourCount) => "Seçilenler: ${count} (${yourCount} sizin seçiminiz)"; - static String m79(name) => "${name} ile selfieler"; - - static String m80(verificationID) => - "İşte ente.io için doğrulama kimliğim: ${verificationID}."; + static String m80(name) => "${name} ile selfieler"; static String m81(verificationID) => + "İşte ente.io için doğrulama kimliğim: ${verificationID}."; + + static String m82(verificationID) => "Merhaba, bu ente.io doğrulama kimliğinizin doğruluğunu onaylayabilir misiniz: ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Ente davet kodu: ${referralCode} \n\nÜcretli hesaba başvurduktan sonra ${referralStorageInGB} GB bedava almak için \nAyarlar → Genel → Davetlerde bu kodu girin\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Belirli kişilerle paylaş', one: '1 kişiyle paylaşıldı', other: '${numberOfPeople} kişiyle paylaşıldı')}"; - static String m84(emailIDs) => "${emailIDs} ile paylaşıldı"; + static String m85(emailIDs) => "${emailIDs} ile paylaşıldı"; - static String m85(fileType) => "Bu ${fileType}, cihazınızdan silinecek."; + static String m86(fileType) => "Bu ${fileType}, cihazınızdan silinecek."; - static String m86(fileType) => + static String m87(fileType) => "${fileType} Ente ve cihazınızdan silinecektir."; - static String m87(fileType) => "${fileType} Ente\'den silinecektir."; + static String m88(fileType) => "${fileType} Ente\'den silinecektir."; - static String m88(name) => "${name} ile spor"; + static String m89(name) => "${name} ile spor"; - static String m89(name) => "Sahne ${name}\'in"; + static String m90(name) => "Sahne ${name}\'in"; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} / ${totalAmount} ${totalStorageUnit} kullanıldı"; - static String m92(id) => + static String m93(id) => "${id}\'niz zaten başka bir ente hesabına bağlı.\n${id} numaranızı bu hesapla kullanmak istiyorsanız lütfen desteğimizle iletişime geçin\'\'"; - static String m93(endDate) => + static String m94(endDate) => "Aboneliğiniz ${endDate} tarihinde iptal edilecektir"; - static String m94(completed, total) => "${completed}/${total} anı korundu"; + static String m95(completed, total) => "${completed}/${total} anı korundu"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "Yüklemek için dokunun, yükleme şu anda ${ignoreReason} nedeniyle yok sayılıyor"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "Aynı zamanda ${storageAmountInGB} GB alıyorlar"; - static String m97(email) => "Bu, ${email}\'in Doğrulama Kimliği"; + static String m98(email) => "Bu, ${email}\'in Doğrulama Kimliği"; - static String m98(count) => + static String m99(count) => "${Intl.plural(count, one: 'Bu hafta, ${count} yıl önce', other: 'Bu hafta, ${count} yıl önce')}"; - static String m99(dateFormat) => "${dateFormat} yıllar boyunca"; + static String m100(dateFormat) => "${dateFormat} yıllar boyunca"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Yakında', one: '1 gün', other: '${count} gün')}"; - static String m101(year) => "${year} yılındaki gezi"; + static String m102(year) => "${year} yılındaki gezi"; - static String m102(location) => "${location}\'a gezi"; + static String m103(location) => "${location}\'a gezi"; - static String m103(email) => + static String m104(email) => "${email} ile eski bir irtibat kişisi olmaya davet edildiniz."; - static String m104(galleryType) => + static String m105(galleryType) => "Galeri türü ${galleryType} yeniden adlandırma için desteklenmiyor"; - static String m105(ignoreReason) => + static String m106(ignoreReason) => "Yükleme ${ignoreReason} nedeniyle yok sayıldı"; - static String m106(count) => "${count} anı korunuyor..."; + static String m107(count) => "${count} anı korunuyor..."; - static String m107(endDate) => "${endDate} tarihine kadar geçerli"; + static String m108(endDate) => "${endDate} tarihine kadar geçerli"; - static String m108(email) => "${email} doğrula"; + static String m109(email) => "${email} doğrula"; - static String m109(count) => - "${Intl.plural(count, zero: '0 izleyici eklendi', one: '1 izleyici eklendi', other: '${count} izleyici eklendi')}"; - - static String m110(email) => - "E-postayı ${email} adresine gönderdik"; + static String m110(name) => + "Bağlantıyı kaldırmak için ${name} kişisini görüntüle"; static String m111(count) => + "${Intl.plural(count, zero: '0 izleyici eklendi', one: '1 izleyici eklendi', other: '${count} izleyici eklendi')}"; + + static String m112(email) => + "E-postayı ${email} adresine gönderdik"; + + static String m113(count) => "${Intl.plural(count, one: '${count} yıl önce', other: '${count} yıl önce')}"; - static String m112(name) => "Sen ve ${name}"; + static String m114(name) => "Sen ve ${name}"; - static String m113(storageSaved) => + static String m115(storageSaved) => "Başarılı bir şekilde ${storageSaved} alanını boşalttınız!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -397,7 +402,7 @@ class MessageLookup extends MessageLookupByLibrary { "Bu, gruptaki ilk fotoğraftır. Seçilen diğer fotoğraflar otomatik olarak bu yeni tarihe göre kaydırılacaktır"), "allow": MessageLookupByLibrary.simpleMessage("İzin ver"), "allowAddPhotosDescription": MessageLookupByLibrary.simpleMessage( - "Bağlantıya sahip olan kişilere, paylaşılan albüme fotoğraf eklemelerine izin ver."), + "Bağlantıya sahip olan kişilerin paylaşılan albüme fotoğraf eklemelerine izin ver."), "allowAddingPhotos": MessageLookupByLibrary.simpleMessage("Fotoğraf eklemeye izin ver"), "allowAppToOpenSharedAlbumLinks": MessageLookupByLibrary.simpleMessage( @@ -540,23 +545,6 @@ class MessageLookup extends MessageLookupByLibrary { "blackFridaySale": MessageLookupByLibrary.simpleMessage("Muhteşem Cuma kampanyası"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLBulkEdit": - MessageLookupByLibrary.simpleMessage("Tarihleri toplu düzenle"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Artık birden fazla fotoğraf seçebilir ve tek bir hızlı işlemle hepsi için tarih/saat düzenleyebilirsiniz. Tarih kaydırma da desteklenmektedir."), - "cLFamilyPlan": - MessageLookupByLibrary.simpleMessage("Aile Planı Sınırları"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Artık aile üyelerinizin ne kadar depolama alanı kullanabileceğine dair sınırlar belirleyebilirsiniz."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Yeni Simge"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Son olarak, çalışmalarımızı en iyi şekilde temsil ettiğini düşündüğümüz yeni bir uygulama simgesi. Eski simgeyi kullanmaya devam edebilmeniz için bir simge değiştirici de ekledik."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Anılar"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Özel anlarınızı yeniden keşfedin - en sevdiğiniz kişilere, seyahatlerinize ve tatillerinize, en iyi tıklamalarınıza ve çok daha fazlasına odaklanın. En iyi deneyim için makine öğrenimini açın, kendinizi etiketleyin ve arkadaşlarınızı adlandırın."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widget\'lar"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Anılarla entegre edilmiş ana ekran widget\'ları artık kullanılabilir. Uygulamayı açmadan özel anlarınızı gösterir."), "cachedData": MessageLookupByLibrary.simpleMessage("Önbelleğe alınmış veriler"), "calculating": MessageLookupByLibrary.simpleMessage("Hesaplanıyor..."), @@ -648,7 +636,7 @@ class MessageLookup extends MessageLookupByLibrary { "collabLinkSectionDescription": MessageLookupByLibrary.simpleMessage( "Ente aplikasyonu veya hesabı olmadan insanların paylaşılan albümde fotoğraf ekleyip görüntülemelerine izin vermek için bir bağlantı oluşturun. Grup veya etkinlik fotoğraflarını toplamak için harika bir seçenek."), "collaborativeLink": - MessageLookupByLibrary.simpleMessage("Organizasyon bağlantısı"), + MessageLookupByLibrary.simpleMessage("Ortak bağlantı"), "collaborativeLinkCreatedFor": m15, "collaborator": MessageLookupByLibrary.simpleMessage("Düzenleyici"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": @@ -697,7 +685,7 @@ class MessageLookup extends MessageLookupByLibrary { "convertToAlbum": MessageLookupByLibrary.simpleMessage("Albüme taşı"), "copyEmailAddress": MessageLookupByLibrary.simpleMessage("E-posta adresini kopyala"), - "copyLink": MessageLookupByLibrary.simpleMessage("Linki kopyala"), + "copyLink": MessageLookupByLibrary.simpleMessage("Bağlantıyı kopyala"), "copypasteThisCodentoYourAuthenticatorApp": MessageLookupByLibrary.simpleMessage( "Bu kodu kopyalayın ve kimlik doğrulama uygulamanıza yapıştırın"), @@ -722,8 +710,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Yeni bir hesap oluşturun"), "createOrSelectAlbum": MessageLookupByLibrary.simpleMessage("Albüm oluştur veya seç"), - "createPublicLink": - MessageLookupByLibrary.simpleMessage("Herkese açık link oluştur"), + "createPublicLink": MessageLookupByLibrary.simpleMessage( + "Herkese açık bir bağlantı oluştur"), "creatingLink": MessageLookupByLibrary.simpleMessage("Bağlantı oluşturuluyor..."), "criticalUpdateAvailable": @@ -735,7 +723,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Güncel kullanımınız "), "currentlyRunning": MessageLookupByLibrary.simpleMessage("şu anda çalışıyor"), - "custom": MessageLookupByLibrary.simpleMessage("Kişisel"), + "custom": MessageLookupByLibrary.simpleMessage("Özel"), "customEndpoint": m20, "darkTheme": MessageLookupByLibrary.simpleMessage("Karanlık"), "dayToday": MessageLookupByLibrary.simpleMessage("Bugün"), @@ -863,6 +851,7 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m25, "duplicateItemsGroup": m26, "edit": MessageLookupByLibrary.simpleMessage("Düzenle"), + "editEmailAlreadyLinked": m27, "editLocation": MessageLookupByLibrary.simpleMessage("Konumu düzenle"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("Konumu düzenle"), @@ -877,16 +866,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-Posta"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "Bu e-posta adresi zaten kayıtlı."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage( "Bu e-posta adresi sistemde kayıtlı değil."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("E-posta doğrulama"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( "Kayıtlarınızı e-postayla gönderin"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage( "Acil Durum İletişim Bilgileri"), "empty": MessageLookupByLibrary.simpleMessage("Boşalt"), @@ -967,7 +956,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Veriyi dışarı aktar"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Ekstra fotoğraflar bulundu"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Yüz henüz kümelenmedi, lütfen daha sonra tekrar gelin"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Yüz Tanıma"), @@ -1002,7 +991,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("Sık sorulan sorular"), "faqs": MessageLookupByLibrary.simpleMessage("Sık Sorulan Sorular"), "favorite": MessageLookupByLibrary.simpleMessage("Favori"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Geri Bildirim"), "file": MessageLookupByLibrary.simpleMessage("Dosya"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -1016,8 +1005,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Dosya türü"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Dosya türleri ve adları"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Dosyalar silinmiş"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( @@ -1035,26 +1024,26 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Yüzler bulundu"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Alınan bedava alan"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Kullanılabilir bedava alan"), "freeTrial": MessageLookupByLibrary.simpleMessage("Ücretsiz deneme"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Cihaz alanını boşaltın"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Zaten yedeklenmiş dosyaları temizleyerek cihazınızda yer kazanın."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Boş alan"), - "freeUpSpaceSaving": m39, + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Galeri"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Galeride 1000\'e kadar anı gösterilir"), "general": MessageLookupByLibrary.simpleMessage("Genel"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Şifreleme anahtarı oluşturuluyor..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Ayarlara git"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( @@ -1082,7 +1071,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Paylaşılan öğeleri ana galeriden gizle"), "hiding": MessageLookupByLibrary.simpleMessage("Gizleniyor..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("OSM Fransa\'da ağırlandı"), "howItWorks": MessageLookupByLibrary.simpleMessage("Nasıl çalışır"), @@ -1140,7 +1129,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Bir şeyler ters gitmiş gibi görünüyor. Lütfen bir süre sonra tekrar deneyin. Hata devam ederse, lütfen destek ekibimizle iletişime geçin."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Öğeler kalıcı olarak silinmeden önce kalan gün sayısını gösterir"), @@ -1161,7 +1150,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Lütfen bu bilgilerle bize yardımcı olun"), "language": MessageLookupByLibrary.simpleMessage("Dil"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("En son güncellenen"), "lastYearsTrip": @@ -1177,7 +1166,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Geleneksel"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Geleneksel hesaplar"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Geleneksel yol, güvendiğiniz kişilerin yokluğunuzda hesabınıza erişmesine olanak tanır."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1187,23 +1176,23 @@ class MessageLookup extends MessageLookupByLibrary { "link": MessageLookupByLibrary.simpleMessage("Bağlantı"), "linkCopiedToClipboard": MessageLookupByLibrary.simpleMessage("Link panoya kopyalandı"), - "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Cihaz limiti"), + "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Cihaz sınırı"), "linkEmail": MessageLookupByLibrary.simpleMessage("E-posta bağlantısı"), "linkEmailToContactBannerCaption": MessageLookupByLibrary.simpleMessage("daha hızlı paylaşım için"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Geçerli"), "linkExpired": MessageLookupByLibrary.simpleMessage("Süresi dolmuş"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": - MessageLookupByLibrary.simpleMessage("Linkin geçerliliği"), + MessageLookupByLibrary.simpleMessage("Bağlantı geçerliliği"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Bağlantının süresi dolmuş"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Asla"), "linkPerson": MessageLookupByLibrary.simpleMessage("Kişiyi bağla"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "daha iyi paylaşım deneyimi için"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Canlı Fotoğraf"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Aboneliğinizi ailenizle paylaşabilirsiniz"), @@ -1282,7 +1271,7 @@ class MessageLookup extends MessageLookupByLibrary { "manageDeviceStorageDesc": MessageLookupByLibrary.simpleMessage( "Yerel önbellek depolama alanını gözden geçirin ve temizleyin."), "manageFamily": MessageLookupByLibrary.simpleMessage("Aileyi yönet"), - "manageLink": MessageLookupByLibrary.simpleMessage("Linki yönet"), + "manageLink": MessageLookupByLibrary.simpleMessage("Bağlantıyı yönet"), "manageParticipants": MessageLookupByLibrary.simpleMessage("Yönet"), "manageSubscription": MessageLookupByLibrary.simpleMessage("Abonelikleri yönet"), @@ -1293,7 +1282,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Ben"), - "memoryCount": m48, + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Ürünler"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Var olan ile birleştir."), @@ -1325,13 +1314,13 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("En son"), "mostRelevant": MessageLookupByLibrary.simpleMessage("En alakalı"), "mountains": MessageLookupByLibrary.simpleMessage("Tepelerin ötesinde"), - "moveItem": m49, + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Seçilen fotoğrafları bir tarihe taşıma"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Albüme taşı"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Gizli albüme ekle"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Cöp kutusuna taşı"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1384,10 +1373,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Sonuç bulunamadı"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Hiçbir sonuç bulunamadı"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("Sistem kilidi bulunamadı"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("Bu kişi değil mi?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( @@ -1396,11 +1385,11 @@ class MessageLookup extends MessageLookupByLibrary { "Burada görülecek bir şey yok! 👀"), "notifications": MessageLookupByLibrary.simpleMessage("Bildirimler"), "ok": MessageLookupByLibrary.simpleMessage("Tamam"), - "onDevice": MessageLookupByLibrary.simpleMessage("Bu cihaz"), + "onDevice": MessageLookupByLibrary.simpleMessage("Cihazda"), "onEnte": MessageLookupByLibrary.simpleMessage( "ente üzerinde"), "onTheRoad": MessageLookupByLibrary.simpleMessage("Yeniden yollarda"), - "onlyFamilyAdminCanChangeCode": m53, + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Sadece onlar"), "oops": MessageLookupByLibrary.simpleMessage("Hay aksi"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1430,7 +1419,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("Eşleştirme tamamlandı"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage("Doğrulama hala bekliyor"), "passkey": MessageLookupByLibrary.simpleMessage("Geçiş anahtarı"), @@ -1440,7 +1429,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Şifreniz başarılı bir şekilde değiştirildi"), "passwordLock": MessageLookupByLibrary.simpleMessage("Şifre kilidi"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Parola gücü, parolanın uzunluğu, kullanılan karakterler ve parolanın en çok kullanılan ilk 10.000 parola arasında yer alıp almadığı dikkate alınarak hesaplanır"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1451,7 +1440,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ödeme başarısız oldu"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Maalesef ödemeniz başarısız oldu. Lütfen destekle iletişime geçin, size yardımcı olacağız!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Bekleyen Öğeler"), "pendingSync": MessageLookupByLibrary.simpleMessage("Bekleyen Senkronizasyonlar"), @@ -1464,20 +1453,20 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Kalıcı olarak sil"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "Cihazdan kalıcı olarak silinsin mi?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Kişi Adı"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Tüylü dostlar"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Fotoğraf Açıklaması"), "photoGridSize": MessageLookupByLibrary.simpleMessage("Izgara boyutu"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("fotoğraf"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Fotoğraflar"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Eklediğiniz fotoğraflar albümden kaldırılacak"), - "photosCount": m60, + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "Fotoğraflar göreli zaman farkını korur"), @@ -1487,7 +1476,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("Pin kilidi"), "playOnTv": MessageLookupByLibrary.simpleMessage("Albümü TV\'de oynat"), "playOriginal": MessageLookupByLibrary.simpleMessage("Orijinali oynat"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Akışı oynat"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore aboneliği"), @@ -1500,14 +1489,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Bu hata devam ederse lütfen desteğe başvurun"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Lütfen izin ver"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Lütfen tekrar giriş yapın"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Lütfen kaldırmak için hızlı bağlantıları seçin"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Lütfen tekrar deneyiniz"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1522,7 +1511,7 @@ class MessageLookup extends MessageLookupByLibrary { "Tekrar denemeden önce lütfen bir süre bekleyin"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Lütfen bekleyin, bu biraz zaman alabilir."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("Kayıtlar hazırlanıyor..."), "preserveMore": @@ -1541,11 +1530,11 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Devam edin"), "processed": MessageLookupByLibrary.simpleMessage("İşlenen"), "processing": MessageLookupByLibrary.simpleMessage("İşleniyor"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Videolar işleniyor"), "publicLinkCreated": MessageLookupByLibrary.simpleMessage( - "Herkese açık link oluşturuldu"), + "Herkese açık bağlantı oluşturuldu"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage( "Herkese açık bağlantı aktive edildi"), "queued": MessageLookupByLibrary.simpleMessage("Kuyrukta"), @@ -1555,10 +1544,10 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Uygulamayı puanlayın"), "rateUs": MessageLookupByLibrary.simpleMessage("Bizi değerlendirin"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("\"Ben\"i yeniden atayın"), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Yeniden atanıyor..."), "recover": MessageLookupByLibrary.simpleMessage("Kurtarma"), @@ -1568,7 +1557,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Hesabı kurtar"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Kurtarma başlatıldı"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Kurtarma anahtarı"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1583,12 +1572,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Kurtarma kodu doğrulandı"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Kurtarma anahtarınız, şifrenizi unutmanız durumunda fotoğraflarınızı kurtarmanın tek yoludur. Kurtarma anahtarınızı Ayarlar > Hesap bölümünde bulabilirsiniz.\n\nDoğru kaydettiğinizi doğrulamak için lütfen kurtarma anahtarınızı buraya girin."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Kurtarma başarılı!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Güvenilir bir kişi hesabınıza erişmeye çalışıyor"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Cihazınız, şifrenizi doğrulamak için yeterli güce sahip değil, ancak tüm cihazlarda çalışacak şekilde yeniden oluşturabiliriz.\n\nLütfen kurtarma anahtarınızı kullanarak giriş yapın ve şifrenizi yeniden oluşturun (istediğiniz takdirde aynı şifreyi tekrar kullanabilirsiniz)."), "recreatePasswordTitle": MessageLookupByLibrary.simpleMessage( @@ -1604,7 +1593,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Bu kodu arkadaşlarınıza verin"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Ücretli bir plan için kaydolsunlar"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Arkadaşını davet et"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( @@ -1632,16 +1621,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Favorilerden Kaldır"), "removeInvite": MessageLookupByLibrary.simpleMessage("Davetiyeyi kaldır"), - "removeLink": MessageLookupByLibrary.simpleMessage("Linki kaldır"), + "removeLink": MessageLookupByLibrary.simpleMessage("Bağlantıyı kaldır"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Katılımcıyı kaldır"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Kişi etiketini kaldırın"), - "removePublicLink": - MessageLookupByLibrary.simpleMessage("Herkese açık link oluştur"), - "removePublicLinks": - MessageLookupByLibrary.simpleMessage("Herkese açık link oluştur"), + "removePublicLink": MessageLookupByLibrary.simpleMessage( + "Herkese açık bağlantıyı kaldır"), + "removePublicLinks": MessageLookupByLibrary.simpleMessage( + "Herkese açık bağlantıları kaldır"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( "Kaldırdığınız öğelerden bazıları başkaları tarafından eklenmiştir ve bunlara erişiminizi kaybedeceksiniz"), "removeWithQuestionMark": @@ -1657,7 +1646,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Dosyayı yeniden adlandır"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Abonelik yenileme"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Hata bildir"), "reportBug": MessageLookupByLibrary.simpleMessage("Hata bildir"), "resendEmail": @@ -1669,7 +1658,7 @@ class MessageLookup extends MessageLookupByLibrary { "resetPerson": MessageLookupByLibrary.simpleMessage("Kaldır"), "resetToDefault": MessageLookupByLibrary.simpleMessage("Varsayılana sıfırla"), - "restore": MessageLookupByLibrary.simpleMessage("Yenile"), + "restore": MessageLookupByLibrary.simpleMessage("Geri yükle"), "restoreToAlbum": MessageLookupByLibrary.simpleMessage("Albümü yenile"), "restoringFiles": MessageLookupByLibrary.simpleMessage("Dosyalar geri yükleniyor..."), @@ -1682,7 +1671,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Önerileri inceleyin"), "right": MessageLookupByLibrary.simpleMessage("Sağ"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Döndür"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Sola döndür"), "rotateRight": MessageLookupByLibrary.simpleMessage("Sağa döndür"), @@ -1738,8 +1727,8 @@ class MessageLookup extends MessageLookupByLibrary { "İnsanları davet ettiğinizde onların paylaştığı tüm fotoğrafları burada göreceksiniz"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "İşleme ve senkronizasyon tamamlandığında kişiler burada gösterilecektir"), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Güvenlik"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Uygulamadaki herkese açık albüm bağlantılarını görün"), @@ -1788,13 +1777,13 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Seçili öğeler bu kişiden silinir, ancak kitaplığınızdan silinmez."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, - "selfiesWithThem": m79, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Gönder"), "sendEmail": MessageLookupByLibrary.simpleMessage("E-posta gönder"), "sendInvite": MessageLookupByLibrary.simpleMessage("Davet kodu gönder"), - "sendLink": MessageLookupByLibrary.simpleMessage("Link gönder"), + "sendLink": MessageLookupByLibrary.simpleMessage("Bağlantıyı gönder"), "serverEndpoint": MessageLookupByLibrary.simpleMessage("Sunucu uç noktası"), "sessionExpired": @@ -1815,22 +1804,23 @@ class MessageLookup extends MessageLookupByLibrary { "setupComplete": MessageLookupByLibrary.simpleMessage("Ayarlama işlemi başarılı"), "share": MessageLookupByLibrary.simpleMessage("Paylaş"), - "shareALink": MessageLookupByLibrary.simpleMessage("Linki paylaş"), + "shareALink": + MessageLookupByLibrary.simpleMessage("Bir bağlantı paylaş"), "shareAlbumHint": MessageLookupByLibrary.simpleMessage( "Bir albüm açın ve paylaşmak için sağ üstteki paylaş düğmesine dokunun."), "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Şimdi bir albüm paylaşın"), - "shareLink": MessageLookupByLibrary.simpleMessage("Linki paylaş"), - "shareMyVerificationID": m80, + "shareLink": MessageLookupByLibrary.simpleMessage("Bağlantıyı paylaş"), + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Yalnızca istediğiniz kişilerle paylaşın"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Orijinal kalitede fotoğraf ve videoları kolayca paylaşabilmemiz için Ente\'yi indirin\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Ente kullanıcısı olmayanlar için paylaş"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("İlk albümünüzü paylaşın"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1842,7 +1832,7 @@ class MessageLookup extends MessageLookupByLibrary { "Paylaşılan fotoğrafları ekle"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Birisi parçası olduğunuz paylaşılan bir albüme fotoğraf eklediğinde bildirim alın"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Benimle paylaşılan"), "sharedWithYou": @@ -1860,11 +1850,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Diğer cihazlardan çıkış yap"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Hizmet Şartları\'nı ve Gizlilik Politikası\'nı kabul ediyorum"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage("Tüm albümlerden silinecek."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Geç"), "social": MessageLookupByLibrary.simpleMessage("Sosyal Medya"), "someItemsAreInBothEnteAndYourDevice": @@ -1901,8 +1891,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Yeniden eskiye"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Önce en eski"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Başarılı"), - "sportsWithThem": m88, - "spotlightOnThem": m89, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Sahne senin"), "startAccountRecoveryTitle": @@ -1917,15 +1907,14 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Depolama"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Aile"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Sen"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Depolama sınırı aşıldı"), - "storageUsageInfo": m91, - "streamDetails": - MessageLookupByLibrary.simpleMessage("Yayın detayları"), + "storageUsageInfo": m92, + "streamDetails": MessageLookupByLibrary.simpleMessage("Akış detayları"), "strongStrength": MessageLookupByLibrary.simpleMessage("Güçlü"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Abone ol"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Paylaşımı etkinleştirmek için aktif bir ücretli aboneliğe ihtiyacınız var."), @@ -1943,7 +1932,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Özellik önerin"), "sunrise": MessageLookupByLibrary.simpleMessage("Ufukta"), "support": MessageLookupByLibrary.simpleMessage("Destek"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Senkronizasyon durduruldu"), "syncing": MessageLookupByLibrary.simpleMessage("Eşitleniyor..."), @@ -1955,7 +1944,7 @@ class MessageLookup extends MessageLookupByLibrary { "tapToUnlock": MessageLookupByLibrary.simpleMessage("Açmak için dokun"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Yüklemek için tıklayın"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Bir şeyler ters gitmiş gibi görünüyor. Lütfen bir süre sonra tekrar deneyin. Hata devam ederse, lütfen destek ekibimizle iletişime geçin."), "terminate": MessageLookupByLibrary.simpleMessage("Sonlandır"), @@ -1978,7 +1967,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Bu öğeler cihazınızdan silinecektir."), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage("Tüm albümlerden silinecek."), "thisActionCannotBeUndone": @@ -1996,12 +1985,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bu görselde exif verisi yok"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Bu benim!"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("Doğrulama kimliğiniz"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("Yıllar boyunca bu hafta"), - "thisWeekXYearsAgo": m98, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Bu, sizi aşağıdaki cihazdan çıkış yapacak:"), @@ -2013,7 +2002,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Bu, seçilen tüm hızlı bağlantıların genel bağlantılarını kaldıracaktır."), - "throughTheYears": m99, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "Uygulama kilidini etkinleştirmek için lütfen sistem ayarlarınızda cihaz şifresi veya ekran kilidi ayarlayın."), @@ -2027,13 +2016,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Toplam boyut"), "trash": MessageLookupByLibrary.simpleMessage("Cöp kutusu"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Kes"), - "tripInYear": m101, - "tripToLocation": m102, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Güvenilir kişiler"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Tekrar deneyiniz"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Bu cihaz klasörüne eklenen dosyaları otomatik olarak ente\'ye yüklemek için yedeklemeyi açın."), @@ -2052,7 +2041,7 @@ class MessageLookup extends MessageLookupByLibrary { "İki faktörlü kimlik doğrulama başarıyla sıfırlandı"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("İki faktörlü kurulum"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Arşivden cıkar"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Arşivden Çıkar"), @@ -2077,10 +2066,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Klasör seçimi güncelleniyor..."), "upgrade": MessageLookupByLibrary.simpleMessage("Yükselt"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Dosyalar albüme taşınıyor..."), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("1 anı korunuyor..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -2099,7 +2088,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Seçilen fotoğrafı kullan"), "usedSpace": MessageLookupByLibrary.simpleMessage("Kullanılan alan"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Doğrulama başarısız oldu, lütfen tekrar deneyin"), @@ -2108,7 +2097,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Doğrula"), "verifyEmail": MessageLookupByLibrary.simpleMessage("E-posta adresini doğrulayın"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Doğrula"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Şifrenizi doğrulayın"), @@ -2119,7 +2108,8 @@ class MessageLookup extends MessageLookupByLibrary { "Kurtarma kodu doğrulanıyor..."), "videoInfo": MessageLookupByLibrary.simpleMessage("Video Bilgileri"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("video"), - "videoStreaming": MessageLookupByLibrary.simpleMessage("Video akışı"), + "videoStreaming": + MessageLookupByLibrary.simpleMessage("Akışlandırılabilir videolar"), "videos": MessageLookupByLibrary.simpleMessage("Videolar"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Aktif oturumları görüntüle"), @@ -2133,10 +2123,11 @@ class MessageLookup extends MessageLookupByLibrary { "viewLargeFilesDesc": MessageLookupByLibrary.simpleMessage( "En fazla depolama alanı kullanan dosyaları görüntüleyin."), "viewLogs": MessageLookupByLibrary.simpleMessage("Kayıtları görüntüle"), + "viewPersonToUnlink": m110, "viewRecoveryKey": MessageLookupByLibrary.simpleMessage( "Kurtarma anahtarını görüntüle"), "viewer": MessageLookupByLibrary.simpleMessage("Görüntüleyici"), - "viewersSuccessfullyAdded": m109, + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Aboneliğinizi yönetmek için lütfen web.ente.io adresini ziyaret edin"), "waitingForVerification": @@ -2149,7 +2140,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Henüz sahibi olmadığınız fotoğraf ve albümlerin düzenlenmesini desteklemiyoruz"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Zayıf"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Tekrardan hoşgeldin!"), @@ -2157,7 +2148,7 @@ class MessageLookup extends MessageLookupByLibrary { "whyAddTrustContact": MessageLookupByLibrary.simpleMessage("."), "yearShort": MessageLookupByLibrary.simpleMessage("yıl"), "yearly": MessageLookupByLibrary.simpleMessage("Yıllık"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Evet"), "yesCancel": MessageLookupByLibrary.simpleMessage("Evet, iptal et"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -2172,7 +2163,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage("Evet, kişiyi sıfırla"), "you": MessageLookupByLibrary.simpleMessage("Sen"), - "youAndThem": m112, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("Aile planı kullanıyorsunuz!"), "youAreOnTheLatestVersion": @@ -2191,7 +2182,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Kendinizle paylaşamazsınız"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage("Arşivlenmiş öğeniz yok."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Hesabınız silindi"), "yourMap": MessageLookupByLibrary.simpleMessage("Haritalarınız"), diff --git a/mobile/lib/generated/intl/messages_uk.dart b/mobile/lib/generated/intl/messages_uk.dart index be09ba23a2..ab1dbb6add 100644 --- a/mobile/lib/generated/intl/messages_uk.dart +++ b/mobile/lib/generated/intl/messages_uk.dart @@ -51,9 +51,6 @@ class MessageLookup extends MessageLookupByLibrary { static String m15(albumName) => "Створено спільне посилання для «${albumName}»"; - static String m16(count) => - "${Intl.plural(count, zero: 'Додано 0 співавторів', one: 'Додано 1 співавтор', few: 'Додано ${count} співаторів', many: 'Додано ${count} співаторів', other: 'Додано ${count} співавторів')}"; - static String m17(email, numOfDays) => "Ви збираєтеся додати ${email} як довірений контакт. Вони зможуть відновити ваш обліковий запис, якщо ви будете відсутні протягом ${numOfDays} днів."; @@ -66,7 +63,7 @@ class MessageLookup extends MessageLookupByLibrary { static String m20(endpoint) => "Під\'єднано до ${endpoint}"; static String m21(count) => - "${Intl.plural(count, one: 'Видалено ${count} елемент', few: 'Видалено ${count} елементи', many: 'Видалено ${count} елементів', other: 'Видалено ${count} елементів')}"; + "${Intl.plural(count, one: 'Видалено ${count} елемент', other: 'Видалено ${count} елементів')}"; static String m22(currentlyDeleting, totalCount) => "Видалення ${currentlyDeleting} / ${totalCount}"; @@ -83,156 +80,156 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} файлів, кожен по ${formattedSize}"; - static String m27(newEmail) => "Поштову адресу змінено на ${newEmail}"; + static String m28(newEmail) => "Поштову адресу змінено на ${newEmail}"; - static String m29(email) => + static String m30(email) => "У ${email} немає облікового запису Ente.\n\nНадішліть їм запрошення для обміну фотографіями."; - static String m31(text) => "Знайдено додаткові фотографії для ${text}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: 'Для 1 файлу', other: 'Для ${formattedNumber} файлів')} на цьому пристрої було створено резервну копію"; + static String m32(text) => "Знайдено додаткові фотографії для ${text}"; static String m34(count, formattedNumber) => - "${Intl.plural(count, one: 'Для 1 файлу', few: 'Для ${formattedNumber} файлів', many: 'Для ${formattedNumber} файлів', other: 'Для ${formattedNumber} файлів')} у цьому альбомі було створено резервну копію"; + "${Intl.plural(count, one: 'Для 1 файлу', other: 'Для ${formattedNumber} файлів')} на цьому пристрої було створено резервну копію"; - static String m35(storageAmountInGB) => + static String m35(count, formattedNumber) => + "${Intl.plural(count, one: 'Для 1 файлу', other: 'Для ${formattedNumber} файлів')} у цьому альбомі було створено резервну копію"; + + static String m36(storageAmountInGB) => "${storageAmountInGB} ГБ щоразу, коли хтось оформлює передплату і застосовує ваш код"; - static String m36(endDate) => "Безплатна пробна версія діє до ${endDate}"; + static String m37(endDate) => "Безплатна пробна версія діє до ${endDate}"; - static String m38(sizeInMBorGB) => "Звільніть ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Звільніть ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Обробка ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} елемент', few: '${count} елементи', many: '${count} елементів', other: '${count} елементів')}"; - static String m44(email) => "${email} запросив вас стати довіреною особою"; + static String m45(email) => "${email} запросив вас стати довіреною особою"; - static String m45(expiryTime) => "Посилання закінчується через ${expiryTime}"; + static String m46(expiryTime) => "Посилання закінчується через ${expiryTime}"; - static String m50(albumName) => "Успішно перенесено до «${albumName}»"; + static String m51(albumName) => "Успішно перенесено до «${albumName}»"; - static String m51(personName) => "Немає пропозицій для ${personName}"; + static String m52(personName) => "Немає пропозицій для ${personName}"; - static String m52(name) => "Не ${name}?"; + static String m53(name) => "Не ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Зв\'яжіться з ${familyAdminEmail}, щоб змінити код."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Надійність пароля: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Зверніться до ${providerName}, якщо було знято платіж"; - static String m61(endDate) => + static String m62(endDate) => "Безплатна пробна версія діє до ${endDate}.\nПісля цього ви можете обрати платний план."; - static String m62(toEmail) => "Напишіть нам на ${toEmail}"; + static String m63(toEmail) => "Напишіть нам на ${toEmail}"; - static String m63(toEmail) => "Надішліть журнали на \n${toEmail}"; + static String m64(toEmail) => "Надішліть журнали на \n${toEmail}"; - static String m65(folderName) => "Оброблюємо «${folderName}»..."; + static String m66(folderName) => "Оброблюємо «${folderName}»..."; - static String m66(storeName) => "Оцініть нас в ${storeName}"; + static String m67(storeName) => "Оцініть нас в ${storeName}"; - static String m68(days, email) => + static String m69(days, email) => "Ви зможете отримати доступ до облікового запису через ${days} днів. Повідомлення буде надіслано на ${email}."; - static String m69(email) => + static String m70(email) => "Тепер ви можете відновити обліковий запис ${email}, встановивши новий пароль."; - static String m70(email) => + static String m71(email) => "${email} намагається відновити ваш обліковий запис."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Ви обоє отримуєте ${storageInGB} ГБ* безплатно"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} буде видалено з цього спільного альбому\n\nБудь-які додані вами фото, будуть також видалені з альбому"; - static String m73(endDate) => "Передплата поновиться ${endDate}"; + static String m74(endDate) => "Передплата поновиться ${endDate}"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, one: 'Знайдено ${count} результат', few: 'Знайдено ${count} результати', many: 'Знайдено ${count} результатів', other: 'Знайдено ${count} результати')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Невідповідність довжини розділів: ${snapshotLength} != ${searchLength}"; - static String m77(count) => "${count} вибрано"; + static String m78(count) => "${count} вибрано"; - static String m78(count, yourCount) => "${count} вибрано (${yourCount} ваші)"; - - static String m80(verificationID) => - "Ось мій ідентифікатор підтвердження: ${verificationID} для ente.io."; + static String m79(count, yourCount) => "${count} вибрано (${yourCount} ваші)"; static String m81(verificationID) => + "Ось мій ідентифікатор підтвердження: ${verificationID} для ente.io."; + + static String m82(verificationID) => "Гей, ви можете підтвердити, що це ваш ідентифікатор підтвердження: ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Реферальний код Ente: ${referralCode} \n\nЗастосуйте його в «Налаштування» → «Загальні» → «Реферали», щоб отримати ${referralStorageInGB} ГБ безплатно після переходу на платний тариф\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Поділитися з конкретними людьми', one: 'Поділитися з 1 особою', other: 'Поділитися з ${numberOfPeople} людьми')}"; - static String m84(emailIDs) => "Поділилися з ${emailIDs}"; + static String m85(emailIDs) => "Поділилися з ${emailIDs}"; - static String m85(fileType) => "Цей ${fileType} буде видалено з пристрою."; + static String m86(fileType) => "Цей ${fileType} буде видалено з пристрою."; - static String m86(fileType) => + static String m87(fileType) => "Цей ${fileType} знаходиться і в Ente, і на вашому пристрої."; - static String m87(fileType) => "Цей ${fileType} буде видалено з Ente."; + static String m88(fileType) => "Цей ${fileType} буде видалено з Ente."; - static String m90(storageAmountInGB) => "${storageAmountInGB} ГБ"; + static String m91(storageAmountInGB) => "${storageAmountInGB} ГБ"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} з ${totalAmount} ${totalStorageUnit} використано"; - static String m92(id) => + static String m93(id) => "Ваш ${id} вже пов\'язаний з іншим обліковим записом Ente.\nЯкщо ви хочете використовувати свій ${id} з цим обліковим записом, зверніться до нашої служби підтримки"; - static String m93(endDate) => "Вашу передплату буде скасовано ${endDate}"; + static String m94(endDate) => "Вашу передплату буде скасовано ${endDate}"; - static String m94(completed, total) => + static String m95(completed, total) => "${completed} / ${total} спогадів збережено"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "Натисніть, щоб завантажити; завантаження наразі ігнорується через: ${ignoreReason}"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "Вони також отримують ${storageAmountInGB} ГБ"; - static String m97(email) => "Це ідентифікатор підтвердження пошти ${email}"; + static String m98(email) => "Це ідентифікатор підтвердження пошти ${email}"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Незабаром', one: '1 день', other: '${count} днів')}"; - static String m103(email) => + static String m104(email) => "Ви отримали запрошення стати спадковим контактом від ${email}."; - static String m104(galleryType) => + static String m105(galleryType) => "Тип галереї «${galleryType}» не підтримується для перейменування"; - static String m105(ignoreReason) => + static String m106(ignoreReason) => "Завантаження проігноровано через: ${ignoreReason}"; - static String m106(count) => "Збереження ${count} спогадів..."; + static String m107(count) => "Збереження ${count} спогадів..."; - static String m107(endDate) => "Діє до ${endDate}"; + static String m108(endDate) => "Діє до ${endDate}"; - static String m108(email) => "Підтвердити ${email}"; + static String m109(email) => "Підтвердити ${email}"; - static String m110(email) => "Ми надіслали листа на ${email}"; + static String m112(email) => "Ми надіслали листа на ${email}"; - static String m111(count) => - "${Intl.plural(count, one: '${count} рік тому', few: '${count} роки тому', many: '${count} років тому', other: '${count} років тому')}"; + static String m113(count) => + "${Intl.plural(count, one: '${count} рік тому', other: '${count} років тому')}"; - static String m113(storageSaved) => "Ви успішно звільнили ${storageSaved}!"; + static String m115(storageSaved) => "Ви успішно звільнили ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -541,7 +538,6 @@ class MessageLookup extends MessageLookupByLibrary { "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Співавтори можуть додавати фотографії та відео до спільного альбому."), - "collaboratorsSuccessfullyAdded": m16, "collageLayout": MessageLookupByLibrary.simpleMessage("Макет"), "collageSaved": MessageLookupByLibrary.simpleMessage("Колаж збережено до галереї"), @@ -762,8 +758,8 @@ class MessageLookup extends MessageLookupByLibrary { "eligible": MessageLookupByLibrary.simpleMessage("придатний"), "email": MessageLookupByLibrary.simpleMessage("Адреса електронної пошти"), - "emailChangedTo": m27, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailNoEnteAccount": m30, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Підтвердження через пошту"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( @@ -845,7 +841,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Експортувати дані"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Знайдено додаткові фотографії"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Обличчя ще не згруповані, поверніться пізніше"), "faceRecognition": @@ -895,8 +891,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Типи файлів"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Типи та назви файлів"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Файли видалено"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Файли збережено до галереї"), @@ -912,13 +908,13 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Знайдені обличчя"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Безплатне сховище отримано"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Безплатне сховище можна використовувати"), "freeTrial": MessageLookupByLibrary.simpleMessage("Безплатний пробний період"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Звільніть місце на пристрої"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -929,7 +925,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("Загальні"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Створення ключів шифрування..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Перейти до налаштувань"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), @@ -1010,7 +1006,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Схоже, що щось пішло не так. Спробуйте ще раз через деякий час. Якщо помилка не зникне, зв\'яжіться з нашою командою підтримки."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Елементи показують кількість днів, що залишилися до остаточного видалення"), @@ -1034,7 +1030,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Спадок"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Облікові записи «Спадку»"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "«Спадок» дозволяє довіреним контактам отримати доступ до вашого облікового запису під час вашої відсутності."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1047,7 +1043,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Досягнуто ліміту пристроїв"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Увімкнено"), "linkExpired": MessageLookupByLibrary.simpleMessage("Закінчився"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage( "Термін дії посилання закінчився"), "linkHasExpired": @@ -1175,7 +1171,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Перемістити до альбому"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Перемістити до прихованого альбому"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Переміщено у смітник"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1227,10 +1223,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Немає результатів"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Нічого не знайдено"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Не знайдено системного блокування"), - "notPersonLabel": m52, + "notPersonLabel": m53, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Поки що з вами ніхто не поділився"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage( @@ -1240,7 +1236,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("На пристрої"), "onEnte": MessageLookupByLibrary.simpleMessage("В Ente"), - "onlyFamilyAdminCanChangeCode": m53, + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Тільки вони"), "oops": MessageLookupByLibrary.simpleMessage("От халепа"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1280,7 +1276,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Пароль успішно змінено"), "passwordLock": MessageLookupByLibrary.simpleMessage("Блокування паролем"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Надійність пароля розраховується з урахуванням довжини пароля, використаних символів, а також того, чи входить пароль у топ 10 000 найбільш використовуваних паролів"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1291,7 +1287,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Не вдалося оплатити"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "На жаль, ваш платіж не вдався. Зв\'яжіться зі службою підтримки і ми вам допоможемо!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Елементи на розгляді"), "pendingSync": @@ -1321,7 +1317,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("Блокування PIN-кодом"), "playOnTv": MessageLookupByLibrary.simpleMessage("Відтворити альбом на ТБ"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Передплата Play Store"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1333,14 +1329,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Зверніться до служби підтримки, якщо проблема не зникне"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Надайте дозволи"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Увійдіть знову"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Виберіть посилання для видалення"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Спробуйте ще раз"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1367,7 +1363,7 @@ class MessageLookup extends MessageLookupByLibrary { "privateSharing": MessageLookupByLibrary.simpleMessage("Приватне поширення"), "proceed": MessageLookupByLibrary.simpleMessage("Продовжити"), - "processingImport": m65, + "processingImport": m66, "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Публічне посилання створено"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage( @@ -1378,7 +1374,7 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Оцініть застосунок"), "rateUs": MessageLookupByLibrary.simpleMessage("Оцініть нас"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "recover": MessageLookupByLibrary.simpleMessage("Відновити"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Відновити обліковий запис"), @@ -1387,7 +1383,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Відновити обліковий запис"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Почато відновлення"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Ключ відновлення"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Ключ відновлення скопійовано в буфер обміну"), @@ -1401,12 +1397,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ключ відновлення перевірено"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Ключ відновлення — це єдиний спосіб відновити фотографії, якщо ви забули пароль. Ви можете знайти свій ключ в розділі «Налаштування» > «Обліковий запис».\n\nВведіть ключ відновлення тут, щоб перевірити, чи правильно ви його зберегли."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Відновлення успішне!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Довірений контакт намагається отримати доступ до вашого облікового запису"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Ваш пристрій недостатньо потужний для перевірки пароля, але ми можемо відновити його таким чином, щоб він працював на всіх пристроях.\n\nУвійдіть за допомогою ключа відновлення та відновіть свій пароль (за бажанням ви можете використати той самий ключ знову)."), "recreatePasswordTitle": @@ -1422,7 +1418,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("1. Дайте цей код друзям"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Вони оформлюють передплату"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Реферали"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("Реферали зараз призупинені"), @@ -1454,7 +1450,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Вилучити посилання"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Видалити учасника"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Видалити мітку особи"), "removePublicLink": @@ -1476,7 +1472,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Перейменувати файл"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Поновити передплату"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Повідомити про помилку"), "reportBug": @@ -1556,8 +1552,8 @@ class MessageLookup extends MessageLookupByLibrary { "Запросіть людей, і ви побачите всі фотографії, якими вони поділилися, тут"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Люди будуть показані тут після завершення оброблення та синхронізації"), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Безпека"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Посилання на публічні альбоми в застосунку"), @@ -1589,8 +1585,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Вибрані елементи будуть видалені з усіх альбомів і переміщені в смітник."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "send": MessageLookupByLibrary.simpleMessage("Надіслати"), "sendEmail": MessageLookupByLibrary.simpleMessage( "Надіслати електронного листа"), @@ -1627,16 +1623,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Поділитися альбомом зараз"), "shareLink": MessageLookupByLibrary.simpleMessage("Поділитися посиланням"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Поділіться тільки з тими людьми, якими ви хочете"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Завантажте Ente для того, щоб легко поділитися фотографіями оригінальної якості та відео\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Поділитися з користувачами без Ente"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Поділитися вашим першим альбомом"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1647,7 +1643,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Нові спільні фотографії"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Отримувати сповіщення, коли хтось додасть фото до спільного альбому, в якому ви перебуваєте"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Поділитися зі мною"), "sharedWithYou": @@ -1664,11 +1660,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Вийти на інших пристроях"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Я приймаю умови використання і політику приватності"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Воно буде видалено з усіх альбомів."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Пропустити"), "social": MessageLookupByLibrary.simpleMessage("Соцмережі"), "someItemsAreInBothEnteAndYourDevice": @@ -1716,13 +1712,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Сховище"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Сім\'я"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Ви"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Перевищено ліміт сховища"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("Надійний"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Передплачувати"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Вам потрібна активна передплата, щоб увімкнути спільне поширення."), @@ -1739,7 +1735,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Запропонувати нові функції"), "support": MessageLookupByLibrary.simpleMessage("Підтримка"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Синхронізацію зупинено"), "syncing": MessageLookupByLibrary.simpleMessage("Синхронізуємо..."), @@ -1752,7 +1748,7 @@ class MessageLookup extends MessageLookupByLibrary { "Торкніться, щоби розблокувати"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Натисніть, щоб завантажити"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Схоже, що щось пішло не так. Спробуйте ще раз через деякий час. Якщо помилка не зникне, зв\'яжіться з нашою командою підтримки."), "terminate": MessageLookupByLibrary.simpleMessage("Припинити"), @@ -1775,7 +1771,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Ці елементи будуть видалені з пристрою."), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Вони будуть видалені з усіх альбомів."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1791,7 +1787,7 @@ class MessageLookup extends MessageLookupByLibrary { "Ця поштова адреса вже використовується"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Це зображення не має даних exif"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Це ваш Ідентифікатор підтвердження"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1816,11 +1812,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("всього"), "totalSize": MessageLookupByLibrary.simpleMessage("Загальний розмір"), "trash": MessageLookupByLibrary.simpleMessage("Смітник"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Вирізати"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Довірені контакти"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Спробувати знову"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Увімкніть резервну копію для автоматичного завантаження файлів, доданих до теки пристрою в Ente."), @@ -1838,7 +1834,7 @@ class MessageLookup extends MessageLookupByLibrary { "Двоетапну перевірку успішно скинуто"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Налаштування двоетапної перевірки"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Розархівувати"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Розархівувати альбом"), @@ -1861,10 +1857,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("Оновлення вибору теки..."), "upgrade": MessageLookupByLibrary.simpleMessage("Покращити"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Завантажуємо файли до альбому..."), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Зберігаємо 1 спогад..."), "upto50OffUntil4thDec": @@ -1883,7 +1879,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Використати вибране фото"), "usedSpace": MessageLookupByLibrary.simpleMessage("Використано місця"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Перевірка не вдалася, спробуйте ще раз"), @@ -1892,7 +1888,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Підтвердити"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Підтвердити пошту"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Підтвердження"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Підтвердити ключ доступу"), @@ -1931,7 +1927,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Ми не підтримуємо редагування фотографій та альбомів, якими ви ще не володієте"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Слабкий"), "welcomeBack": MessageLookupByLibrary.simpleMessage("З поверненням!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Що нового"), @@ -1939,7 +1935,7 @@ class MessageLookup extends MessageLookupByLibrary { "Довірений контакт може допомогти у відновленні ваших даних."), "yearShort": MessageLookupByLibrary.simpleMessage("рік"), "yearly": MessageLookupByLibrary.simpleMessage("Щороку"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Так"), "yesCancel": MessageLookupByLibrary.simpleMessage("Так, скасувати"), "yesConvertToViewer": @@ -1972,7 +1968,7 @@ class MessageLookup extends MessageLookupByLibrary { "Ви не можете поділитися із собою"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "У вас немає жодних архівних елементів."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Ваш обліковий запис видалено"), "yourMap": MessageLookupByLibrary.simpleMessage("Ваша мапа"), diff --git a/mobile/lib/generated/intl/messages_vi.dart b/mobile/lib/generated/intl/messages_vi.dart index 4d6fd4a225..d3b3fe023b 100644 --- a/mobile/lib/generated/intl/messages_vi.dart +++ b/mobile/lib/generated/intl/messages_vi.dart @@ -84,160 +84,160 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} tệp, ${formattedSize} mỗi tệp"; - static String m27(newEmail) => "Email đã được thay đổi thành ${newEmail}"; + static String m28(newEmail) => "Email đã được thay đổi thành ${newEmail}"; - static String m29(email) => + static String m30(email) => "${email} không có tài khoản Ente.\n\nGửi cho họ một lời mời để chia sẻ ảnh."; - static String m31(text) => "Extra photos found for ${text}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 tệp', other: '${formattedNumber} tệp')} trên thiết bị này đã được sao lưu an toàn"; + static String m32(text) => "Extra photos found for ${text}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 tệp', other: '${formattedNumber} tệp')} trên thiết bị này đã được sao lưu an toàn"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 tệp', other: '${formattedNumber} tệp')} trong album này đã được sao lưu an toàn"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB mỗi khi ai đó đăng ký gói trả phí và áp dụng mã của bạn"; - static String m36(endDate) => "Dùng thử miễn phí có hiệu lực đến ${endDate}"; + static String m37(endDate) => "Dùng thử miễn phí có hiệu lực đến ${endDate}"; - static String m38(sizeInMBorGB) => "Giải phóng ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Giải phóng ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Đang xử lý ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} mục', other: '${count} mục')}"; - static String m44(email) => + static String m45(email) => "${email} đã mời bạn trở thành một liên hệ tin cậy"; - static String m45(expiryTime) => "Liên kết sẽ hết hạn vào ${expiryTime}"; + static String m46(expiryTime) => "Liên kết sẽ hết hạn vào ${expiryTime}"; - static String m50(albumName) => "Đã di chuyển thành công đến ${albumName}"; + static String m51(albumName) => "Đã di chuyển thành công đến ${albumName}"; - static String m51(personName) => "Không có gợi ý cho ${personName}"; + static String m52(personName) => "Không có gợi ý cho ${personName}"; - static String m52(name) => "Không phải ${name}?"; + static String m53(name) => "Không phải ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Vui lòng liên hệ ${familyAdminEmail} để thay đổi mã của bạn."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Độ mạnh mật khẩu: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Vui lòng nói chuyện với bộ phận hỗ trợ ${providerName} nếu bạn đã bị tính phí"; - static String m61(endDate) => + static String m62(endDate) => "Dùng thử miễn phí có hiệu lực đến ${endDate}.\nBạn có thể chọn gói trả phí sau đó."; - static String m62(toEmail) => + static String m63(toEmail) => "Vui lòng gửi email cho chúng tôi tại ${toEmail}"; - static String m63(toEmail) => "Vui lòng gửi nhật ký đến \n${toEmail}"; + static String m64(toEmail) => "Vui lòng gửi nhật ký đến \n${toEmail}"; - static String m65(folderName) => "Đang xử lý ${folderName}..."; + static String m66(folderName) => "Đang xử lý ${folderName}..."; - static String m66(storeName) => "Đánh giá chúng tôi trên ${storeName}"; + static String m67(storeName) => "Đánh giá chúng tôi trên ${storeName}"; - static String m68(days, email) => + static String m69(days, email) => "Bạn có thể truy cập tài khoản sau ${days} ngày. Một thông báo sẽ được gửi đến ${email}."; - static String m69(email) => + static String m70(email) => "Bạn có thể khôi phục tài khoản của ${email} bằng cách đặt lại mật khẩu mới."; - static String m70(email) => + static String m71(email) => "${email} đang cố gắng khôi phục tài khoản của bạn."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Cả hai bạn đều nhận ${storageInGB} GB* miễn phí"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} sẽ bị xóa khỏi album chia sẻ này\n\nBất kỳ ảnh nào được thêm bởi họ cũng sẽ bị xóa khỏi album"; - static String m73(endDate) => "Đăng ký sẽ được gia hạn vào ${endDate}"; + static String m74(endDate) => "Đăng ký sẽ được gia hạn vào ${endDate}"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} kết quả được tìm thấy', other: '${count} kết quả được tìm thấy')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Độ dài các phần không khớp: ${snapshotLength} != ${searchLength}"; - static String m77(count) => "${count} đã chọn"; + static String m78(count) => "${count} đã chọn"; - static String m78(count, yourCount) => + static String m79(count, yourCount) => "${count} đã chọn (${yourCount} của bạn)"; - static String m80(verificationID) => + static String m81(verificationID) => "Đây là ID xác minh của tôi: ${verificationID} cho ente.io."; - static String m81(verificationID) => + static String m82(verificationID) => "Chào, bạn có thể xác nhận rằng đây là ID xác minh ente.io của bạn: ${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Mã giới thiệu Ente: ${referralCode} \n\nÁp dụng nó trong Cài đặt → Chung → Giới thiệu để nhận ${referralStorageInGB} GB miễn phí sau khi bạn đăng ký gói trả phí\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Chia sẻ với những người cụ thể', one: 'Chia sẻ với 1 người', other: 'Chia sẻ với ${numberOfPeople} người')}"; - static String m84(emailIDs) => "Chia sẻ với ${emailIDs}"; - - static String m85(fileType) => - "Tệp ${fileType} này sẽ bị xóa khỏi thiết bị của bạn."; + static String m85(emailIDs) => "Chia sẻ với ${emailIDs}"; static String m86(fileType) => + "Tệp ${fileType} này sẽ bị xóa khỏi thiết bị của bạn."; + + static String m87(fileType) => "Tệp ${fileType} này có trong cả Ente và thiết bị của bạn."; - static String m87(fileType) => "Tệp ${fileType} này sẽ bị xóa khỏi Ente."; + static String m88(fileType) => "Tệp ${fileType} này sẽ bị xóa khỏi Ente."; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} trong tổng số ${totalAmount} ${totalStorageUnit} đã sử dụng"; - static String m92(id) => + static String m93(id) => "ID ${id} của bạn đã được liên kết với một tài khoản Ente khác.\nNếu bạn muốn sử dụng ID ${id} này với tài khoản này, vui lòng liên hệ với bộ phận hỗ trợ của chúng tôi."; - static String m93(endDate) => "Đăng ký của bạn sẽ bị hủy vào ${endDate}"; + static String m94(endDate) => "Đăng ký của bạn sẽ bị hủy vào ${endDate}"; - static String m94(completed, total) => + static String m95(completed, total) => "${completed}/${total} kỷ niệm đã được lưu giữ"; - static String m95(ignoreReason) => + static String m96(ignoreReason) => "Nhấn để tải lên, tải lên hiện tại bị bỏ qua do ${ignoreReason}"; - static String m96(storageAmountInGB) => + static String m97(storageAmountInGB) => "Họ cũng nhận được ${storageAmountInGB} GB"; - static String m97(email) => "Đây là ID xác minh của ${email}"; + static String m98(email) => "Đây là ID xác minh của ${email}"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Soon', one: '1 day', other: '${count} days')}"; - static String m103(email) => + static String m104(email) => "Bạn đã được mời làm người liên hệ thừa kế bởi ${email}."; - static String m104(galleryType) => + static String m105(galleryType) => "Loại thư viện ${galleryType} không được hỗ trợ để đổi tên"; - static String m105(ignoreReason) => "Tải lên bị bỏ qua do ${ignoreReason}"; + static String m106(ignoreReason) => "Tải lên bị bỏ qua do ${ignoreReason}"; - static String m106(count) => "Đang lưu giữ ${count} kỷ niệm..."; + static String m107(count) => "Đang lưu giữ ${count} kỷ niệm..."; - static String m107(endDate) => "Có hiệu lực đến ${endDate}"; + static String m108(endDate) => "Có hiệu lực đến ${endDate}"; - static String m108(email) => "Xác minh ${email}"; + static String m109(email) => "Xác minh ${email}"; - static String m110(email) => + static String m112(email) => "Chúng tôi đã gửi một email đến ${email}"; - static String m111(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} năm trước', other: '${count} năm trước')}"; - static String m113(storageSaved) => + static String m115(storageSaved) => "Bạn đã giải phóng thành công ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -758,8 +758,8 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("Email"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("Email đã được đăng kí."), - "emailChangedTo": m27, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("Email chưa được đăng kí."), "emailVerificationToggle": @@ -840,7 +840,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Xuất dữ liệu của bạn"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Tìm thấy ảnh bổ sung"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Khuôn mặt chưa được phân cụm, vui lòng quay lại sau"), "faceRecognition": @@ -889,8 +889,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Loại tệp"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Loại tệp và tên"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Tệp đã bị xóa"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( "Các tệp đã được lưu vào thư viện"), @@ -906,12 +906,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Đã tìm thấy khuôn mặt"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Lưu trữ miễn phí đã yêu cầu"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Lưu trữ miễn phí có thể sử dụng"), "freeTrial": MessageLookupByLibrary.simpleMessage("Dùng thử miễn phí"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Giải phóng không gian thiết bị"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -924,7 +924,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("Chung"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage("Đang tạo khóa mã hóa..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Đi đến cài đặt"), "googlePlayId": MessageLookupByLibrary.simpleMessage("ID Google Play"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( @@ -1005,7 +1005,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Có vẻ như đã xảy ra sự cố. Vui lòng thử lại sau một thời gian. Nếu lỗi vẫn tiếp diễn, vui lòng liên hệ với đội ngũ hỗ trợ của chúng tôi."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Các mục cho biết số ngày còn lại trước khi xóa vĩnh viễn"), @@ -1035,7 +1035,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Thừa kế"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Tài khoản thừa kế"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Thừa kế cho phép các liên hệ tin cậy truy cập tài khoản của bạn khi bạn không hoạt động."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1048,7 +1048,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Giới hạn thiết bị"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Đã bật"), "linkExpired": MessageLookupByLibrary.simpleMessage("Hết hạn"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Hết hạn liên kết"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Liên kết đã hết hạn"), @@ -1168,7 +1168,7 @@ class MessageLookup extends MessageLookupByLibrary { "moveToAlbum": MessageLookupByLibrary.simpleMessage("Chuyển đến album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Di chuyển đến album ẩn"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Đã chuyển vào thùng rác"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1221,10 +1221,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Không có kết quả"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Không tìm thấy kết quả"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Không tìm thấy khóa hệ thống"), - "notPersonLabel": m52, + "notPersonLabel": m53, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Chưa có gì được chia sẻ với bạn"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage( @@ -1234,7 +1234,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Trên thiết bị"), "onEnte": MessageLookupByLibrary.simpleMessage( "Trên ente"), - "onlyFamilyAdminCanChangeCode": m53, + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Chỉ họ"), "oops": MessageLookupByLibrary.simpleMessage("Ôi"), "oopsCouldNotSaveEdits": @@ -1271,7 +1271,7 @@ class MessageLookup extends MessageLookupByLibrary { "Đã thay đổi mật khẩu thành công"), "passwordLock": MessageLookupByLibrary.simpleMessage("Khóa bằng mật khẩu"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Độ mạnh của mật khẩu được tính toán dựa trên độ dài của mật khẩu, các ký tự đã sử dụng và liệu mật khẩu có xuất hiện trong 10.000 mật khẩu được sử dụng nhiều nhất hay không"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1282,7 +1282,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Thanh toán thất bại"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Rất tiếc, thanh toán của bạn đã thất bại. Vui lòng liên hệ với bộ phận hỗ trợ và chúng tôi sẽ giúp bạn!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Các mục đang chờ"), "pendingSync": @@ -1310,7 +1310,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinAlbum": MessageLookupByLibrary.simpleMessage("Ghim album"), "pinLock": MessageLookupByLibrary.simpleMessage("Khóa PIN"), "playOnTv": MessageLookupByLibrary.simpleMessage("Phát album trên TV"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Đăng ký PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1322,14 +1322,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Vui lòng liên hệ với bộ phận hỗ trợ nếu vấn đề vẫn tiếp diễn"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Vui lòng cấp quyền"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Vui lòng đăng nhập lại"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Vui lòng chọn liên kết nhanh để xóa"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Vui lòng thử lại"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1358,7 +1358,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Chia sẻ riêng tư"), "proceed": MessageLookupByLibrary.simpleMessage("Tiếp tục"), "processed": MessageLookupByLibrary.simpleMessage("Đã xử lý"), - "processingImport": m65, + "processingImport": m66, "publicLinkCreated": MessageLookupByLibrary.simpleMessage( "Liên kết công khai đã được tạo"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage( @@ -1368,7 +1368,7 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Tạo vé"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Đánh giá ứng dụng"), "rateUs": MessageLookupByLibrary.simpleMessage("Đánh giá chúng tôi"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "recover": MessageLookupByLibrary.simpleMessage("Khôi phục"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Khôi phục tài khoản"), @@ -1377,7 +1377,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Khôi phục tài khoản"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage( "Quá trình khôi phục đã được khởi động"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Khóa khôi phục"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Khóa khôi phục đã được sao chép vào clipboard"), @@ -1391,12 +1391,12 @@ class MessageLookup extends MessageLookupByLibrary { "Khóa khôi phục đã được xác minh"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Khóa khôi phục của bạn là cách duy nhất để khôi phục ảnh của bạn nếu bạn quên mật khẩu. Bạn có thể tìm thấy khóa khôi phục của mình trong Cài đặt > Tài khoản.\n\nVui lòng nhập khóa khôi phục của bạn ở đây để xác minh rằng bạn đã lưu nó đúng cách."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Khôi phục thành công!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Một liên hệ tin cậy đang cố gắng truy cập tài khoản của bạn"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Thiết bị hiện tại không đủ mạnh để xác minh mật khẩu của bạn, nhưng chúng tôi có thể tạo lại theo cách hoạt động với tất cả các thiết bị.\n\nVui lòng đăng nhập bằng khóa khôi phục của bạn và tạo lại mật khẩu (bạn có thể sử dụng lại mật khẩu cũ nếu muốn)."), "recreatePasswordTitle": @@ -1411,7 +1411,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Đưa mã này cho bạn bè của bạn"), "referralStep2": MessageLookupByLibrary.simpleMessage("2. Họ đăng ký gói trả phí"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Giới thiệu"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Giới thiệu hiện đang tạm dừng"), @@ -1440,7 +1440,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Xóa liên kết"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Xóa người tham gia"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Xóa nhãn người"), "removePublicLink": @@ -1459,7 +1459,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Đổi tên tệp"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Gia hạn đăng ký"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Báo cáo lỗi"), "reportBug": MessageLookupByLibrary.simpleMessage("Báo cáo lỗi"), "resendEmail": MessageLookupByLibrary.simpleMessage("Gửi lại email"), @@ -1535,8 +1535,8 @@ class MessageLookup extends MessageLookupByLibrary { "Mời mọi người, và bạn sẽ thấy tất cả ảnh được chia sẻ bởi họ ở đây"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Người sẽ được hiển thị ở đây sau khi hoàn tất xử lý và đồng bộ"), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Bảo mật"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Xem liên kết album công khai trong ứng dụng"), @@ -1569,8 +1569,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Các mục đã chọn sẽ bị xóa khỏi tất cả các album và chuyển vào thùng rác."), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "send": MessageLookupByLibrary.simpleMessage("Gửi"), "sendEmail": MessageLookupByLibrary.simpleMessage("Gửi email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Gửi lời mời"), @@ -1601,16 +1601,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage( "Chia sẻ một album ngay bây giờ"), "shareLink": MessageLookupByLibrary.simpleMessage("Chia sẻ liên kết"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Chia sẻ chỉ với những người bạn muốn"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Tải Ente để chúng ta có thể dễ dàng chia sẻ ảnh và video chất lượng gốc\n\nhttps://ente.io"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Chia sẻ với người dùng không phải Ente"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Chia sẻ album đầu tiên của bạn"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1622,7 +1622,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ảnh chia sẻ mới"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Nhận thông báo khi ai đó thêm ảnh vào album chia sẻ mà bạn tham gia"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Chia sẻ với tôi"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Được chia sẻ với bạn"), @@ -1638,11 +1638,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Đăng xuất các thiết bị khác"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Tôi đồng ý với các điều khoản dịch vụchính sách bảo mật"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Nó sẽ bị xóa khỏi tất cả các album."), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Bỏ qua"), "social": MessageLookupByLibrary.simpleMessage("Xã hội"), "someItemsAreInBothEnteAndYourDevice": @@ -1689,13 +1689,13 @@ class MessageLookup extends MessageLookupByLibrary { "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Gia đình"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Bạn"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Vượt quá giới hạn lưu trữ"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("Mạnh"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Đăng ký"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Bạn cần một đăng ký trả phí hoạt động để kích hoạt chia sẻ."), @@ -1712,7 +1712,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Gợi ý tính năng"), "support": MessageLookupByLibrary.simpleMessage("Hỗ trợ"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Đồng bộ hóa đã dừng"), "syncing": MessageLookupByLibrary.simpleMessage("Đang đồng bộ hóa..."), @@ -1722,7 +1722,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Chạm để nhập mã"), "tapToUnlock": MessageLookupByLibrary.simpleMessage("Nhấn để mở khóa"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Nhấn để tải lên"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Có vẻ như đã xảy ra sự cố. Vui lòng thử lại sau một thời gian. Nếu lỗi vẫn tiếp diễn, vui lòng liên hệ với đội ngũ hỗ trợ."), "terminate": MessageLookupByLibrary.simpleMessage("Kết thúc"), @@ -1746,7 +1746,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Các mục này sẽ bị xóa khỏi thiết bị của bạn."), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Chúng sẽ bị xóa khỏi tất cả các album."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1762,7 +1762,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Email này đã được sử dụng"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Hình ảnh này không có dữ liệu exif"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("Đây là ID xác minh của bạn"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1786,11 +1786,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("tổng"), "totalSize": MessageLookupByLibrary.simpleMessage("Tổng kích thước"), "trash": MessageLookupByLibrary.simpleMessage("Thùng rác"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Cắt"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Liên hệ tin cậy"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Thử lại"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Bật sao lưu để tự động tải lên các tệp được thêm vào thư mục thiết bị này lên Ente."), @@ -1809,7 +1809,7 @@ class MessageLookup extends MessageLookupByLibrary { "Xác thực hai yếu tố đã được đặt lại thành công"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Cài đặt hai yếu tố"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Khôi phục"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Khôi phục album"), @@ -1833,10 +1833,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Đang cập nhật lựa chọn thư mục..."), "upgrade": MessageLookupByLibrary.simpleMessage("Nâng cấp"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage("Đang tải tệp lên album..."), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Đang lưu giữ 1 kỷ niệm..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1854,14 +1854,14 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sử dụng ảnh đã chọn"), "usedSpace": MessageLookupByLibrary.simpleMessage("Không gian đã sử dụng"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Xác minh không thành công, vui lòng thử lại"), "verificationId": MessageLookupByLibrary.simpleMessage("ID xác minh"), "verify": MessageLookupByLibrary.simpleMessage("Xác minh"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Xác minh email"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Xác minh"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Xác minh mã khóa"), @@ -1899,7 +1899,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Chúng tôi không hỗ trợ chỉnh sửa ảnh và album mà bạn chưa sở hữu"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Yếu"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Chào mừng trở lại!"), @@ -1908,7 +1908,7 @@ class MessageLookup extends MessageLookupByLibrary { "Liên hệ tin cậy có thể giúp khôi phục dữ liệu của bạn."), "yearShort": MessageLookupByLibrary.simpleMessage("năm"), "yearly": MessageLookupByLibrary.simpleMessage("Hàng năm"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Có"), "yesCancel": MessageLookupByLibrary.simpleMessage("Có, hủy"), "yesConvertToViewer": @@ -1940,7 +1940,7 @@ class MessageLookup extends MessageLookupByLibrary { "Bạn không thể chia sẻ với chính mình"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Bạn không có mục nào đã lưu trữ."), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Tài khoản của bạn đã bị xóa"), "yourMap": MessageLookupByLibrary.simpleMessage("Bản đồ của bạn"), diff --git a/mobile/lib/generated/intl/messages_zh.dart b/mobile/lib/generated/intl/messages_zh.dart index 396c964b16..0ae7085ffb 100644 --- a/mobile/lib/generated/intl/messages_zh.dart +++ b/mobile/lib/generated/intl/messages_zh.dart @@ -93,203 +93,203 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} 个文件,每个文件 ${formattedSize}"; - static String m27(newEmail) => "电子邮件已更改为 ${newEmail}"; + static String m28(newEmail) => "电子邮件已更改为 ${newEmail}"; - static String m28(email) => "${email} 没有 Ente 账户。"; + static String m29(email) => "${email} 没有 Ente 账户。"; - static String m29(email) => "${email} 没有 Ente 帐户。\n\n向他们发出共享照片的邀请。"; + static String m30(email) => "${email} 没有 Ente 帐户。\n\n向他们发出共享照片的邀请。"; - static String m30(name) => "拥抱 ${name}"; + static String m31(name) => "拥抱 ${name}"; - static String m31(text) => "为 ${text} 找到额外照片"; + static String m32(text) => "为 ${text} 找到额外照片"; - static String m32(name) => "与 ${name} 的盛宴"; - - static String m33(count, formattedNumber) => - "此设备上的 ${Intl.plural(count, one: '1 个文件', other: '${formattedNumber} 个文件')} 已安全备份"; + static String m33(name) => "与 ${name} 的盛宴"; static String m34(count, formattedNumber) => + "此设备上的 ${Intl.plural(count, one: '1 个文件', other: '${formattedNumber} 个文件')} 已安全备份"; + + static String m35(count, formattedNumber) => "此相册中的 ${Intl.plural(count, one: '1 个文件', other: '${formattedNumber} 个文件')} 已安全备份"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "每当有人使用您的代码注册付费计划时您将获得${storageAmountInGB} GB"; - static String m36(endDate) => "免费试用有效期至 ${endDate}"; + static String m37(endDate) => "免费试用有效期至 ${endDate}"; - static String m37(count) => + static String m38(count) => "只要您拥有有效订阅,您仍然可以在 Ente 上访问 ${Intl.plural(count, one: '它', other: '它们')}"; - static String m38(sizeInMBorGB) => "释放 ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "释放 ${sizeInMBorGB}"; - static String m39(count, formattedSize) => + static String m40(count, formattedSize) => "${Intl.plural(count, one: '它可以从设备中删除以释放 ${formattedSize}', other: '它们可以从设备中删除以释放 ${formattedSize}')}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "正在处理 ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "与 ${name} 徒步"; + static String m42(name) => "与 ${name} 徒步"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} 个项目', other: '${count} 个项目')}"; - static String m43(name) => "最后一次与 ${name} 相聚"; + static String m44(name) => "最后一次与 ${name} 相聚"; - static String m44(email) => "${email} 已邀请您成为可信联系人"; + static String m45(email) => "${email} 已邀请您成为可信联系人"; - static String m45(expiryTime) => "链接将在 ${expiryTime} 过期"; + static String m46(expiryTime) => "链接将在 ${expiryTime} 过期"; - static String m46(email) => "将人员链接到 ${email}"; + static String m47(email) => "将人员链接到 ${email}"; - static String m47(personName, email) => "这将会将 ${personName} 链接到 ${email}"; + static String m48(personName, email) => "这将会将 ${personName} 链接到 ${email}"; - static String m48(count, formattedCount) => + static String m49(count, formattedCount) => "${Intl.plural(count, zero: '暂无回忆', other: '${formattedCount} 个回忆')}"; - static String m49(count) => + static String m50(count) => "${Intl.plural(count, one: '移动项目', other: '移动数个项目')}"; - static String m50(albumName) => "成功移动到 ${albumName}"; + static String m51(albumName) => "成功移动到 ${albumName}"; - static String m51(personName) => "没有针对 ${personName} 的建议"; + static String m52(personName) => "没有针对 ${personName} 的建议"; - static String m52(name) => "不是 ${name}?"; + static String m53(name) => "不是 ${name}?"; - static String m53(familyAdminEmail) => "请联系${familyAdminEmail} 以更改您的代码。"; + static String m54(familyAdminEmail) => "请联系${familyAdminEmail} 以更改您的代码。"; - static String m54(name) => "与 ${name} 开派对"; + static String m55(name) => "与 ${name} 开派对"; - static String m55(passwordStrengthValue) => "密码强度: ${passwordStrengthValue}"; + static String m56(passwordStrengthValue) => "密码强度: ${passwordStrengthValue}"; - static String m56(providerName) => "如果您被收取费用,请用英语与 ${providerName} 的客服聊天"; + static String m57(providerName) => "如果您被收取费用,请用英语与 ${providerName} 的客服聊天"; - static String m57(name, age) => "${name} ${age} 岁啦!"; + static String m58(name, age) => "${name} ${age} 岁啦!"; - static String m58(name, age) => "${name} 快满 ${age} 岁啦"; - - static String m59(count) => - "${Intl.plural(count, zero: '没有照片', one: '1 张照片', other: '${count} 张照片')}"; + static String m59(name, age) => "${name} 快满 ${age} 岁啦"; static String m60(count) => + "${Intl.plural(count, zero: '没有照片', one: '1 张照片', other: '${count} 张照片')}"; + + static String m61(count) => "${Intl.plural(count, zero: '0张照片', one: '1张照片', other: '${count} 张照片')}"; - static String m61(endDate) => "免费试用有效期至 ${endDate}。\n在此之后您可以选择付费计划。"; + static String m62(endDate) => "免费试用有效期至 ${endDate}。\n在此之后您可以选择付费计划。"; - static String m62(toEmail) => "请给我们发送电子邮件至 ${toEmail}"; + static String m63(toEmail) => "请给我们发送电子邮件至 ${toEmail}"; - static String m63(toEmail) => "请将日志发送至 \n${toEmail}"; + static String m64(toEmail) => "请将日志发送至 \n${toEmail}"; - static String m64(name) => "与 ${name} 的合影"; + static String m65(name) => "与 ${name} 的合影"; - static String m65(folderName) => "正在处理 ${folderName}..."; + static String m66(folderName) => "正在处理 ${folderName}..."; - static String m66(storeName) => "在 ${storeName} 上给我们评分"; + static String m67(storeName) => "在 ${storeName} 上给我们评分"; - static String m67(name) => "已将您重新分配给 ${name}"; + static String m68(name) => "已将您重新分配给 ${name}"; - static String m68(days, email) => "您可以在 ${days} 天后访问该账户。通知将发送至 ${email}。"; + static String m69(days, email) => "您可以在 ${days} 天后访问该账户。通知将发送至 ${email}。"; - static String m69(email) => "您现在可以通过设置新密码来恢复 ${email} 的账户。"; + static String m70(email) => "您现在可以通过设置新密码来恢复 ${email} 的账户。"; - static String m70(email) => "${email} 正在尝试恢复您的账户。"; + static String m71(email) => "${email} 正在尝试恢复您的账户。"; - static String m71(storageInGB) => "3. 你和朋友都将免费获得 ${storageInGB} GB*"; + static String m72(storageInGB) => "3. 你和朋友都将免费获得 ${storageInGB} GB*"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} 将从这个共享相册中删除\n\nTA们添加的任何照片也将从相册中删除"; - static String m73(endDate) => "在 ${endDate} 前续费"; + static String m74(endDate) => "在 ${endDate} 前续费"; - static String m74(name) => "与 ${name} 一起的自驾游"; + static String m75(name) => "与 ${name} 一起的自驾游"; - static String m75(count) => + static String m76(count) => "${Intl.plural(count, other: '已找到 ${count} 个结果')}"; - static String m76(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "部分长度不匹配:${snapshotLength} != ${searchLength}"; - static String m77(count) => "已选择 ${count} 个"; + static String m78(count) => "已选择 ${count} 个"; - static String m78(count, yourCount) => "选择了 ${count} 个 (您的 ${yourCount} 个)"; + static String m79(count, yourCount) => "选择了 ${count} 个 (您的 ${yourCount} 个)"; - static String m79(name) => "与 ${name} 的自拍"; + static String m80(name) => "与 ${name} 的自拍"; - static String m80(verificationID) => "这是我的ente.io 的验证 ID: ${verificationID}。"; + static String m81(verificationID) => "这是我的ente.io 的验证 ID: ${verificationID}。"; - static String m81(verificationID) => + static String m82(verificationID) => "嘿,你能确认这是你的 ente.io 验证 ID吗:${verificationID}"; - static String m82(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Ente 推荐代码:${referralCode}\n\n在 \"设置\"→\"通用\"→\"推荐 \"中应用它,即可在注册付费计划后免费获得 ${referralStorageInGB} GB 存储空间\n\nhttps://ente.io"; - static String m83(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: '与特定人员共享', one: '与 1 人共享', other: '与 ${numberOfPeople} 人共享')}"; - static String m84(emailIDs) => "与 ${emailIDs} 共享"; + static String m85(emailIDs) => "与 ${emailIDs} 共享"; - static String m85(fileType) => "此 ${fileType} 将从您的设备中删除。"; + static String m86(fileType) => "此 ${fileType} 将从您的设备中删除。"; - static String m86(fileType) => "${fileType} 已同时存在于 Ente 和您的设备中。"; + static String m87(fileType) => "${fileType} 已同时存在于 Ente 和您的设备中。"; - static String m87(fileType) => "${fileType} 将从 Ente 中删除。"; + static String m88(fileType) => "${fileType} 将从 Ente 中删除。"; - static String m88(name) => "与 ${name} 一起运动"; + static String m89(name) => "与 ${name} 一起运动"; - static String m89(name) => "聚光灯下的 ${name}"; + static String m90(name) => "聚光灯下的 ${name}"; - static String m90(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m91( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "已使用 ${usedAmount} ${usedStorageUnit} / ${totalAmount} ${totalStorageUnit}"; - static String m92(id) => + static String m93(id) => "您的 ${id} 已链接到另一个 Ente 账户。\n如果您想在此账户中使用您的 ${id} ,请联系我们的支持人员"; - static String m93(endDate) => "您的订阅将于 ${endDate} 取消"; + static String m94(endDate) => "您的订阅将于 ${endDate} 取消"; - static String m94(completed, total) => "已保存的回忆 ${completed}/共 ${total}"; + static String m95(completed, total) => "已保存的回忆 ${completed}/共 ${total}"; - static String m95(ignoreReason) => "点按上传,由于${ignoreReason},目前上传已被忽略"; + static String m96(ignoreReason) => "点按上传,由于${ignoreReason},目前上传已被忽略"; - static String m96(storageAmountInGB) => "他们也会获得 ${storageAmountInGB} GB"; + static String m97(storageAmountInGB) => "他们也会获得 ${storageAmountInGB} GB"; - static String m97(email) => "这是 ${email} 的验证ID"; + static String m98(email) => "这是 ${email} 的验证ID"; - static String m98(count) => + static String m99(count) => "${Intl.plural(count, one: '${count} 年前的本周', other: '${count} 年前的本周')}"; - static String m99(dateFormat) => "${dateFormat} 年间"; + static String m100(dateFormat) => "${dateFormat} 年间"; - static String m100(count) => + static String m101(count) => "${Intl.plural(count, zero: '马上', one: '1 天', other: '${count} 天')}"; - static String m101(year) => "${year} 年的旅行"; + static String m102(year) => "${year} 年的旅行"; - static String m102(location) => "前往 ${location} 的旅行"; + static String m103(location) => "前往 ${location} 的旅行"; - static String m103(email) => "您已受邀通过 ${email} 成为遗产联系人。"; + static String m104(email) => "您已受邀通过 ${email} 成为遗产联系人。"; - static String m104(galleryType) => "相册类型 ${galleryType} 不支持重命名"; + static String m105(galleryType) => "相册类型 ${galleryType} 不支持重命名"; - static String m105(ignoreReason) => "由于 ${ignoreReason},上传被忽略"; + static String m106(ignoreReason) => "由于 ${ignoreReason},上传被忽略"; - static String m106(count) => "正在保存 ${count} 个回忆..."; + static String m107(count) => "正在保存 ${count} 个回忆..."; - static String m107(endDate) => "有效期至 ${endDate}"; + static String m108(endDate) => "有效期至 ${endDate}"; - static String m108(email) => "验证 ${email}"; - - static String m109(count) => - "${Intl.plural(count, zero: '已添加0个查看者', one: '已添加1个查看者', other: '已添加 ${count} 个查看者')}"; - - static String m110(email) => "我们已经发送邮件到 ${email}"; + static String m109(email) => "验证 ${email}"; static String m111(count) => + "${Intl.plural(count, zero: '已添加0个查看者', one: '已添加1个查看者', other: '已添加 ${count} 个查看者')}"; + + static String m112(email) => "我们已经发送邮件到 ${email}"; + + static String m113(count) => "${Intl.plural(count, one: '${count} 年前', other: '${count} 年前')}"; - static String m112(name) => "您和 ${name}"; + static String m114(name) => "您和 ${name}"; - static String m113(storageSaved) => "您已成功释放了 ${storageSaved}!"; + static String m115(storageSaved) => "您已成功释放了 ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -479,21 +479,6 @@ class MessageLookup extends MessageLookupByLibrary { "birthday": MessageLookupByLibrary.simpleMessage("生日"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("黑色星期五特惠"), "blog": MessageLookupByLibrary.simpleMessage("博客"), - "cLBulkEdit": MessageLookupByLibrary.simpleMessage("批量编辑日期"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "你现在可以选择多张照片,一键批量修改日期/时间,并支持日期顺移。"), - "cLFamilyPlan": MessageLookupByLibrary.simpleMessage("家庭计划存储限制"), - "cLFamilyPlanDesc": - MessageLookupByLibrary.simpleMessage("你现在可以为家庭成员设置存储空间使用上限。"), - "cLIcon": MessageLookupByLibrary.simpleMessage("新图标"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "终于迎来了一个全新的应用图标,我们认为它最能代表我们的作品。同时,我们还添加了图标切换功能,所以您可以继续使用旧图标。"), - "cLMemories": MessageLookupByLibrary.simpleMessage("回忆"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "重新发现你的珍贵时刻——聚焦你最爱的亲友、旅行与假期、美妙瞬间等精彩回忆。启用机器学习,标记自己并为朋友命名,享受最佳体验。"), - "cLWidgets": MessageLookupByLibrary.simpleMessage("小组件"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "全新首页小组件,与回忆深度集成。无需打开应用,即可在主屏幕上查看你的特别时刻。"), "cachedData": MessageLookupByLibrary.simpleMessage("缓存数据"), "calculating": MessageLookupByLibrary.simpleMessage("正在计算..."), "canNotOpenBody": @@ -739,15 +724,15 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("电子邮件地址"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("此电子邮件地址已被注册。"), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("此电子邮件地址未被注册。"), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("电子邮件验证"), "emailYourLogs": MessageLookupByLibrary.simpleMessage("通过电子邮件发送您的日志"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("紧急联系人"), "empty": MessageLookupByLibrary.simpleMessage("清空"), "emptyTrash": MessageLookupByLibrary.simpleMessage("要清空回收站吗?"), @@ -809,7 +794,7 @@ class MessageLookup extends MessageLookupByLibrary { "exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"), "exportYourData": MessageLookupByLibrary.simpleMessage("导出您的数据"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("发现额外照片"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage("人脸尚未聚类,请稍后再来"), "faceRecognition": MessageLookupByLibrary.simpleMessage("人脸识别"), @@ -838,7 +823,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("常见问题"), "faqs": MessageLookupByLibrary.simpleMessage("常见问题"), "favorite": MessageLookupByLibrary.simpleMessage("收藏"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("反馈"), "file": MessageLookupByLibrary.simpleMessage("文件"), "fileFailedToSaveToGallery": @@ -848,8 +833,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileSavedToGallery": MessageLookupByLibrary.simpleMessage("文件已保存到相册"), "fileTypes": MessageLookupByLibrary.simpleMessage("文件类型"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("文件类型和名称"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("文件已删除"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("多个文件已保存到相册"), @@ -861,24 +846,24 @@ class MessageLookup extends MessageLookupByLibrary { "forgotPassword": MessageLookupByLibrary.simpleMessage("忘记密码"), "foundFaces": MessageLookupByLibrary.simpleMessage("已找到的人脸"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("已领取的免费存储"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("可用的免费存储"), "freeTrial": MessageLookupByLibrary.simpleMessage("免费试用"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("释放设备空间"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage("通过清除已备份的文件来节省设备空间。"), "freeUpSpace": MessageLookupByLibrary.simpleMessage("释放空间"), - "freeUpSpaceSaving": m39, + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("图库"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage("在图库中显示最多1000个回忆"), "general": MessageLookupByLibrary.simpleMessage("通用"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage("正在生成加密密钥..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("前往设置"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), "grantFullAccessPrompt": @@ -904,7 +889,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage("隐藏主页图库中的共享项目"), "hiding": MessageLookupByLibrary.simpleMessage("正在隐藏..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("法国 OSM 主办"), "howItWorks": MessageLookupByLibrary.simpleMessage("工作原理"), "howToViewShareeVerificationID": MessageLookupByLibrary.simpleMessage( @@ -952,7 +937,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "看起来出了点问题。 请稍后重试。 如果错误仍然存在,请联系我们的支持团队。"), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage("项目显示永久删除前剩余的天数"), "itemsWillBeRemovedFromAlbum": @@ -970,7 +955,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage("请帮助我们了解这个信息"), "language": MessageLookupByLibrary.simpleMessage("语言"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("最后更新"), "lastYearsTrip": MessageLookupByLibrary.simpleMessage("去年的旅行"), "leave": MessageLookupByLibrary.simpleMessage("离开"), @@ -980,7 +965,7 @@ class MessageLookup extends MessageLookupByLibrary { "left": MessageLookupByLibrary.simpleMessage("向左"), "legacy": MessageLookupByLibrary.simpleMessage("遗产"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("遗产账户"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage("遗产允许信任的联系人在您不在时访问您的账户。"), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -996,14 +981,14 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("来实现更快的共享"), "linkEnabled": MessageLookupByLibrary.simpleMessage("已启用"), "linkExpired": MessageLookupByLibrary.simpleMessage("已过期"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("链接过期"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("链接已过期"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("永不"), "linkPerson": MessageLookupByLibrary.simpleMessage("链接人员"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage("来感受更好的共享体验"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("实况照片"), "loadMessage1": MessageLookupByLibrary.simpleMessage("您可以与家庭分享您的订阅"), "loadMessage3": @@ -1074,7 +1059,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("我"), - "memoryCount": m48, + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("商品"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("与现有的合并"), "mergedPhotos": MessageLookupByLibrary.simpleMessage("已合并照片"), @@ -1101,12 +1086,12 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("最近"), "mostRelevant": MessageLookupByLibrary.simpleMessage("最相关"), "mountains": MessageLookupByLibrary.simpleMessage("翻过山丘"), - "moveItem": m49, + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage("将选定的照片调整到某一日期"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("移动到相册"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("移至隐藏相册"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("已移至回收站"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage("正在将文件移动到相册..."), @@ -1150,9 +1135,9 @@ class MessageLookup extends MessageLookupByLibrary { "由于我们端到端加密协议的性质,如果没有您的密码或恢复密钥,您的数据将无法解密"), "noResults": MessageLookupByLibrary.simpleMessage("无结果"), "noResultsFound": MessageLookupByLibrary.simpleMessage("未找到任何结果"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("未找到系统锁"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("不是此人?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage("尚未与您共享任何内容"), @@ -1163,7 +1148,7 @@ class MessageLookup extends MessageLookupByLibrary { "onEnte": MessageLookupByLibrary.simpleMessage( "在 ente 上"), "onTheRoad": MessageLookupByLibrary.simpleMessage("再次踏上旅途"), - "onlyFamilyAdminCanChangeCode": m53, + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("仅限他们"), "oops": MessageLookupByLibrary.simpleMessage("哎呀"), "oopsCouldNotSaveEdits": @@ -1190,7 +1175,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairWithPin": MessageLookupByLibrary.simpleMessage("用 PIN 配对"), "pairingComplete": MessageLookupByLibrary.simpleMessage("配对完成"), "panorama": MessageLookupByLibrary.simpleMessage("全景"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage("仍需进行验证"), "passkey": MessageLookupByLibrary.simpleMessage("通行密钥"), @@ -1199,7 +1184,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("密码修改成功"), "passwordLock": MessageLookupByLibrary.simpleMessage("密码锁"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "密码强度的计算考虑了密码的长度、使用的字符以及密码是否出现在最常用的 10,000 个密码中"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1208,7 +1193,7 @@ class MessageLookup extends MessageLookupByLibrary { "paymentFailed": MessageLookupByLibrary.simpleMessage("支付失败"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "不幸的是,您的付款失败。请联系支持人员,我们将为您提供帮助!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("待处理项目"), "pendingSync": MessageLookupByLibrary.simpleMessage("正在等待同步"), "people": MessageLookupByLibrary.simpleMessage("人物"), @@ -1218,18 +1203,18 @@ class MessageLookup extends MessageLookupByLibrary { "permanentlyDelete": MessageLookupByLibrary.simpleMessage("永久删除"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage("要从设备中永久删除吗?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("人物名称"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("毛茸茸的伙伴"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("照片说明"), "photoGridSize": MessageLookupByLibrary.simpleMessage("照片网格大小"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("照片"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("照片"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage("您添加的照片将从相册中移除"), - "photosCount": m60, + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage("照片保持相对时间差"), "pickCenterPoint": MessageLookupByLibrary.simpleMessage("选择中心点"), @@ -1237,7 +1222,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("PIN 锁定"), "playOnTv": MessageLookupByLibrary.simpleMessage("在电视上播放相册"), "playOriginal": MessageLookupByLibrary.simpleMessage("播放原内容"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("播放流"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore 订阅"), @@ -1248,12 +1233,12 @@ class MessageLookup extends MessageLookupByLibrary { "请用英语联系 support@ente.io ,我们将乐意提供帮助!"), "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage("如果问题仍然存在,请联系支持"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("请授予权限"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("请重新登录"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage("请选择要删除的快速链接"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("请重试"), "pleaseVerifyTheCodeYouHaveEntered": MessageLookupByLibrary.simpleMessage("请验证您输入的代码"), @@ -1264,7 +1249,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("请稍等片刻后再重试"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage("请稍候,这将需要一段时间。"), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("正在准备日志..."), "preserveMore": MessageLookupByLibrary.simpleMessage("保留更多"), "pressAndHoldToPlayVideo": @@ -1279,7 +1264,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("继续"), "processed": MessageLookupByLibrary.simpleMessage("已处理"), "processing": MessageLookupByLibrary.simpleMessage("正在处理"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("正在处理视频"), "publicLinkCreated": MessageLookupByLibrary.simpleMessage("公共链接已创建"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage("公开链接已启用"), @@ -1289,16 +1274,16 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("提升工单"), "rateTheApp": MessageLookupByLibrary.simpleMessage("为此应用评分"), "rateUs": MessageLookupByLibrary.simpleMessage("给我们评分"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("重新分配“我”"), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("正在重新分配..."), "recover": MessageLookupByLibrary.simpleMessage("恢复"), "recoverAccount": MessageLookupByLibrary.simpleMessage("恢复账户"), "recoverButton": MessageLookupByLibrary.simpleMessage("恢复"), "recoveryAccount": MessageLookupByLibrary.simpleMessage("恢复账户"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("已启动恢复"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("恢复密钥"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage("恢复密钥已复制到剪贴板"), @@ -1311,11 +1296,11 @@ class MessageLookup extends MessageLookupByLibrary { "recoveryKeyVerified": MessageLookupByLibrary.simpleMessage("恢复密钥已验证"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "如果您忘记了密码,恢复密钥是恢复照片的唯一方法。您可以在“设置”>“账户”中找到恢复密钥。\n\n请在此处输入恢复密钥,以验证您是否已正确保存。"), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("恢复成功!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage("一位可信联系人正在尝试访问您的账户"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "当前设备的功能不足以验证您的密码,但我们可以以适用于所有设备的方式重新生成。\n\n请使用您的恢复密钥登录并重新生成您的密码(如果您希望,可以再次使用相同的密码)。"), "recreatePasswordTitle": MessageLookupByLibrary.simpleMessage("重新创建密码"), @@ -1326,7 +1311,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("把我们推荐给你的朋友然后获得延长一倍的订阅计划"), "referralStep1": MessageLookupByLibrary.simpleMessage("1. 将此代码提供给您的朋友"), "referralStep2": MessageLookupByLibrary.simpleMessage("2. 他们注册一个付费计划"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("推荐"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("推荐已暂停"), @@ -1349,7 +1334,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeInvite": MessageLookupByLibrary.simpleMessage("移除邀请"), "removeLink": MessageLookupByLibrary.simpleMessage("移除链接"), "removeParticipant": MessageLookupByLibrary.simpleMessage("移除参与者"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("移除人物标签"), "removePublicLink": MessageLookupByLibrary.simpleMessage("删除公开链接"), "removePublicLinks": MessageLookupByLibrary.simpleMessage("删除公开链接"), @@ -1364,7 +1349,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameAlbum": MessageLookupByLibrary.simpleMessage("重命名相册"), "renameFile": MessageLookupByLibrary.simpleMessage("重命名文件"), "renewSubscription": MessageLookupByLibrary.simpleMessage("续费订阅"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("报告错误"), "reportBug": MessageLookupByLibrary.simpleMessage("报告错误"), "resendEmail": MessageLookupByLibrary.simpleMessage("重新发送电子邮件"), @@ -1382,7 +1367,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("请检查并删除您认为重复的项目。"), "reviewSuggestions": MessageLookupByLibrary.simpleMessage("查看建议"), "right": MessageLookupByLibrary.simpleMessage("向右"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("旋转"), "rotateLeft": MessageLookupByLibrary.simpleMessage("向左旋转"), "rotateRight": MessageLookupByLibrary.simpleMessage("向右旋转"), @@ -1427,8 +1412,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("邀请他人,您将在此看到他们分享的所有照片"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage("处理和同步完成后,人物将显示在此处"), - "searchResultCount": m75, - "searchSectionsLengthMismatch": m76, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("安全"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage("在应用程序中查看公开相册链接"), @@ -1464,9 +1449,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("所选项目将从所有相册中删除并移动到回收站。"), "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage("选定的项目将从此人身上移除,但不会从您的库中删除。"), - "selectedPhotos": m77, - "selectedPhotosWithYours": m78, - "selfiesWithThem": m79, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("发送"), "sendEmail": MessageLookupByLibrary.simpleMessage("发送电子邮件"), "sendInvite": MessageLookupByLibrary.simpleMessage("发送邀请"), @@ -1489,16 +1474,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("打开相册并点击右上角的分享按钮进行分享"), "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("立即分享相册"), "shareLink": MessageLookupByLibrary.simpleMessage("分享链接"), - "shareMyVerificationID": m80, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage("仅与您想要的人分享"), - "shareTextConfirmOthersVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage("下载 Ente,让我们轻松共享高质量的原始照片和视频"), - "shareTextReferralCode": m82, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage("与非 Ente 用户共享"), - "shareWithPeopleSectionTitle": m83, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("分享您的第一个相册"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1509,7 +1494,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("新共享的照片"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage("当有人将照片添加到您所属的共享相册时收到通知"), - "sharedWith": m84, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("与我共享"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("已与您共享"), "sharing": MessageLookupByLibrary.simpleMessage("正在分享..."), @@ -1523,11 +1508,11 @@ class MessageLookup extends MessageLookupByLibrary { "signOutOtherDevices": MessageLookupByLibrary.simpleMessage("登出其他设备"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "我同意 服务条款隐私政策"), - "singleFileDeleteFromDevice": m85, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage("它将从所有相册中删除。"), - "singleFileInBothLocalAndRemote": m86, - "singleFileInRemoteOnly": m87, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("跳过"), "social": MessageLookupByLibrary.simpleMessage("社交"), "someItemsAreInBothEnteAndYourDevice": @@ -1554,8 +1539,8 @@ class MessageLookup extends MessageLookupByLibrary { "sortNewestFirst": MessageLookupByLibrary.simpleMessage("最新在前"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("最旧在前"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ 成功"), - "sportsWithThem": m88, - "spotlightOnThem": m89, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("聚光灯下的自己"), "startAccountRecoveryTitle": MessageLookupByLibrary.simpleMessage("开始恢复"), @@ -1566,13 +1551,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("存储空间"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("家庭"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("您"), - "storageInGB": m90, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("已超出存储限制"), - "storageUsageInfo": m91, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("流详情"), "strongStrength": MessageLookupByLibrary.simpleMessage("强"), - "subAlreadyLinkedErrMessage": m92, - "subWillBeCancelledOn": m93, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("订阅"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage("您需要有效的付费订阅才能启用共享。"), @@ -1586,7 +1571,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("建议新功能"), "sunrise": MessageLookupByLibrary.simpleMessage("在地平线上"), "support": MessageLookupByLibrary.simpleMessage("支持"), - "syncProgress": m94, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("同步已停止"), "syncing": MessageLookupByLibrary.simpleMessage("正在同步···"), "systemTheme": MessageLookupByLibrary.simpleMessage("适应系统"), @@ -1594,7 +1579,7 @@ class MessageLookup extends MessageLookupByLibrary { "tapToEnterCode": MessageLookupByLibrary.simpleMessage("点击以输入代码"), "tapToUnlock": MessageLookupByLibrary.simpleMessage("点击解锁"), "tapToUpload": MessageLookupByLibrary.simpleMessage("点按上传"), - "tapToUploadIsIgnoredDue": m95, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "看起来出了点问题。 请稍后重试。 如果错误仍然存在,请联系我们的支持团队。"), @@ -1614,7 +1599,7 @@ class MessageLookup extends MessageLookupByLibrary { "theme": MessageLookupByLibrary.simpleMessage("主题"), "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage("这些项目将从您的设备中删除。"), - "theyAlsoGetXGb": m96, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage("他们将从所有相册中删除。"), "thisActionCannotBeUndone": @@ -1629,11 +1614,11 @@ class MessageLookup extends MessageLookupByLibrary { "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage("此图像没有Exif 数据"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("这就是我!"), - "thisIsPersonVerificationId": m97, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("这是您的验证 ID"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("历年本周"), - "thisWeekXYearsAgo": m98, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage("这将使您在以下设备中退出登录:"), "thisWillLogYouOutOfThisDevice": @@ -1642,7 +1627,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("这将使所有选定的照片的日期和时间相同。"), "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage("这将删除所有选定的快速链接的公共链接。"), - "throughTheYears": m99, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage("要启用应用锁,请在系统设置中设置设备密码或屏幕锁。"), "toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage("隐藏照片或视频"), @@ -1654,12 +1639,12 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("总计"), "totalSize": MessageLookupByLibrary.simpleMessage("总大小"), "trash": MessageLookupByLibrary.simpleMessage("回收站"), - "trashDaysLeft": m100, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("修剪"), - "tripInYear": m101, - "tripToLocation": m102, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("可信联系人"), - "trustedInviteBody": m103, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("请再试一次"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "打开备份可自动上传添加到此设备文件夹的文件至 Ente。"), @@ -1674,7 +1659,7 @@ class MessageLookup extends MessageLookupByLibrary { "twofactorAuthenticationSuccessfullyReset": MessageLookupByLibrary.simpleMessage("成功重置双重认证"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("双重认证设置"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m104, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("取消存档"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("取消存档相册"), "unarchiving": MessageLookupByLibrary.simpleMessage("正在取消存档..."), @@ -1694,10 +1679,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("正在更新文件夹选择..."), "upgrade": MessageLookupByLibrary.simpleMessage("升级"), - "uploadIsIgnoredDueToIgnorereason": m105, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage("正在将文件上传到相册..."), - "uploadingMultipleMemories": m106, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("正在保存 1 个回忆..."), "upto50OffUntil4thDec": @@ -1712,13 +1697,13 @@ class MessageLookup extends MessageLookupByLibrary { "useRecoveryKey": MessageLookupByLibrary.simpleMessage("使用恢复密钥"), "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("使用所选照片"), "usedSpace": MessageLookupByLibrary.simpleMessage("已用空间"), - "validTill": m107, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage("验证失败,请重试"), "verificationId": MessageLookupByLibrary.simpleMessage("验证 ID"), "verify": MessageLookupByLibrary.simpleMessage("验证"), "verifyEmail": MessageLookupByLibrary.simpleMessage("验证电子邮件"), - "verifyEmailID": m108, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("验证"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("验证通行密钥"), "verifyPassword": MessageLookupByLibrary.simpleMessage("验证密码"), @@ -1727,7 +1712,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("正在验证恢复密钥..."), "videoInfo": MessageLookupByLibrary.simpleMessage("视频详情"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("视频"), - "videoStreaming": MessageLookupByLibrary.simpleMessage("影音流"), "videos": MessageLookupByLibrary.simpleMessage("视频"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("查看活动会话"), "viewAddOnButton": MessageLookupByLibrary.simpleMessage("查看附加组件"), @@ -1739,7 +1723,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewLogs": MessageLookupByLibrary.simpleMessage("查看日志"), "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("查看恢复密钥"), "viewer": MessageLookupByLibrary.simpleMessage("查看者"), - "viewersSuccessfullyAdded": m109, + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage("请访问 web.ente.io 来管理您的订阅"), "waitingForVerification": @@ -1749,7 +1733,7 @@ class MessageLookup extends MessageLookupByLibrary { "weAreOpenSource": MessageLookupByLibrary.simpleMessage("我们是开源的 !"), "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage("我们不支持编辑您尚未拥有的照片和相册"), - "weHaveSendEmailTo": m110, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("弱"), "welcomeBack": MessageLookupByLibrary.simpleMessage("欢迎回来!"), "whatsNew": MessageLookupByLibrary.simpleMessage("更新日志"), @@ -1757,7 +1741,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("可信联系人可以帮助恢复您的数据。"), "yearShort": MessageLookupByLibrary.simpleMessage("年"), "yearly": MessageLookupByLibrary.simpleMessage("每年"), - "yearsAgo": m111, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("是"), "yesCancel": MessageLookupByLibrary.simpleMessage("是的,取消"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage("是的,转换为查看者"), @@ -1768,7 +1752,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesRenew": MessageLookupByLibrary.simpleMessage("是的,续费"), "yesResetPerson": MessageLookupByLibrary.simpleMessage("是,重设人物"), "you": MessageLookupByLibrary.simpleMessage("您"), - "youAndThem": m112, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("你在一个家庭计划中!"), "youAreOnTheLatestVersion": @@ -1785,7 +1769,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("莫开玩笑,您不能与自己分享"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage("您没有任何存档的项目。"), - "youHaveSuccessfullyFreedUp": m113, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("您的账户已删除"), "yourMap": MessageLookupByLibrary.simpleMessage("您的地图"), diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart index 85f95e667b..b580aada38 100644 --- a/mobile/lib/generated/l10n.dart +++ b/mobile/lib/generated/l10n.dart @@ -60,6 +60,16 @@ class S { ); } + /// `Enter your new email address` + String get enterYourNewEmailAddress { + return Intl.message( + 'Enter your new email address', + name: 'enterYourNewEmailAddress', + desc: '', + args: [], + ); + } + /// `Welcome back!` String get accountWelcomeBack { return Intl.message( @@ -9096,6 +9106,26 @@ class S { ); } + /// `This email is already linked to {name}.` + String editEmailAlreadyLinked(Object name) { + return Intl.message( + 'This email is already linked to $name.', + name: 'editEmailAlreadyLinked', + desc: '', + args: [name], + ); + } + + /// `View {name} to unlink` + String viewPersonToUnlink(Object name) { + return Intl.message( + 'View $name to unlink', + name: 'viewPersonToUnlink', + desc: '', + args: [name], + ); + } + /// `Enter name` String get enterName { return Intl.message( @@ -11081,10 +11111,10 @@ class S { ); } - /// `Generate streamable video` + /// `Streamable videos` String get videoStreaming { return Intl.message( - 'Generate streamable video', + 'Streamable videos', name: 'videoStreaming', desc: '', args: [], @@ -11676,111 +11706,221 @@ class S { ); } - /// `New Icon` - String get cLIcon { + /// `Curated memories` + String get curatedMemories { return Intl.message( - 'New Icon', - name: 'cLIcon', - desc: '', - args: [], - ); - } - - /// `Finally, a new app icon, that we think best represents our work. We've also added an icon-switcher so you can continue using the old icon.` - String get cLIconDesc { - return Intl.message( - 'Finally, a new app icon, that we think best represents our work. We\'ve also added an icon-switcher so you can continue using the old icon.', - name: 'cLIconDesc', - desc: '', - args: [], - ); - } - - /// `Memories` - String get cLMemories { - return Intl.message( - 'Memories', - name: 'cLMemories', - desc: '', - args: [], - ); - } - - /// `Rediscover your special moments - spotlight on your favorite people, your trips and holidays, your best clicks, and much more. Turn on machine learning, tag yourself and name your friends for the best experience.` - String get cLMemoriesDesc { - return Intl.message( - 'Rediscover your special moments - spotlight on your favorite people, your trips and holidays, your best clicks, and much more. Turn on machine learning, tag yourself and name your friends for the best experience.', - name: 'cLMemoriesDesc', + 'Curated memories', + name: 'curatedMemories', desc: '', args: [], ); } /// `Widgets` - String get cLWidgets { + String get widgets { return Intl.message( 'Widgets', - name: 'cLWidgets', + name: 'widgets', desc: '', args: [], ); } - /// `Home screen widgets that are integrated with memories are now available. They will show your special moments without opening the app.` - String get cLWidgetsDesc { + /// `Memories` + String get memories { return Intl.message( - 'Home screen widgets that are integrated with memories are now available. They will show your special moments without opening the app.', - name: 'cLWidgetsDesc', + 'Memories', + name: 'memories', desc: '', args: [], ); } - /// `Family Plan Limits` - String get cLFamilyPlan { + /// `Select the people you wish to see on your homescreen.` + String get peopleWidgetDesc { return Intl.message( - 'Family Plan Limits', - name: 'cLFamilyPlan', + 'Select the people you wish to see on your homescreen.', + name: 'peopleWidgetDesc', desc: '', args: [], ); } - /// `You can now set limits on how much storage your family members can use.` - String get cLFamilyPlanDesc { + /// `Select the albums you wish to see on your homescreen.` + String get albumsWidgetDesc { return Intl.message( - 'You can now set limits on how much storage your family members can use.', - name: 'cLFamilyPlanDesc', + 'Select the albums you wish to see on your homescreen.', + name: 'albumsWidgetDesc', desc: '', args: [], ); } - /// `Bulk Edit dates` - String get cLBulkEdit { + /// `Select the kind of memories you wish to see on your homescreen.` + String get memoriesWidgetDesc { return Intl.message( - 'Bulk Edit dates', - name: 'cLBulkEdit', + 'Select the kind of memories you wish to see on your homescreen.', + name: 'memoriesWidgetDesc', desc: '', args: [], ); } - /// `You can now select multiple photos, and edit date/time for all of them with one quick action. Shifting dates is also supported.` - String get cLBulkEditDesc { + /// `Smart memories` + String get smartMemories { return Intl.message( - 'You can now select multiple photos, and edit date/time for all of them with one quick action. Shifting dates is also supported.', - name: 'cLBulkEditDesc', + 'Smart memories', + name: 'smartMemories', desc: '', args: [], ); } - /// `Curated memories` - String get curatedMemories { + /// `Past years' memories` + String get pastYearsMemories { return Intl.message( - 'Curated memories', - name: 'curatedMemories', + 'Past years\' memories', + name: 'pastYearsMemories', + desc: '', + args: [], + ); + } + + /// `Also delete the photos (and videos) present in these {count} albums from all other albums they are part of?` + String deleteMultipleAlbumDialog(Object count) { + return Intl.message( + 'Also delete the photos (and videos) present in these $count albums from all other albums they are part of?', + name: 'deleteMultipleAlbumDialog', + desc: '', + args: [count], + ); + } + + /// `Add participants` + String get addParticipants { + return Intl.message( + 'Add participants', + name: 'addParticipants', + desc: '', + args: [], + ); + } + + /// `{count} selected` + String selectedAlbums(Object count) { + return Intl.message( + '$count selected', + name: 'selectedAlbums', + desc: '', + args: [count], + ); + } + + /// `Action not supported on Favourites album` + String get actionNotSupportedOnFavouritesAlbum { + return Intl.message( + 'Action not supported on Favourites album', + name: 'actionNotSupportedOnFavouritesAlbum', + desc: '', + args: [], + ); + } + + /// `On this day memories` + String get onThisDayMemories { + return Intl.message( + 'On this day memories', + name: 'onThisDayMemories', + desc: '', + args: [], + ); + } + + /// `On this day` + String get onThisDay { + return Intl.message( + 'On this day', + name: 'onThisDay', + desc: '', + args: [], + ); + } + + /// `Look back on your memories 🌄` + String get lookBackOnYourMemories { + return Intl.message( + 'Look back on your memories 🌄', + name: 'lookBackOnYourMemories', + desc: '', + args: [], + ); + } + + /// ` new 📸` + String get newPhotosEmoji { + return Intl.message( + ' new 📸', + name: 'newPhotosEmoji', + desc: '', + args: [], + ); + } + + /// `Sorry, we had to pause your backups` + String get sorryWeHadToPauseYourBackups { + return Intl.message( + 'Sorry, we had to pause your backups', + name: 'sorryWeHadToPauseYourBackups', + desc: '', + args: [], + ); + } + + /// `Click to install our best version yet` + String get clickToInstallOurBestVersionYet { + return Intl.message( + 'Click to install our best version yet', + name: 'clickToInstallOurBestVersionYet', + desc: '', + args: [], + ); + } + + /// `Receive reminders about memories from this day in previous years.` + String get onThisDayNotificationExplanation { + return Intl.message( + 'Receive reminders about memories from this day in previous years.', + name: 'onThisDayNotificationExplanation', + desc: '', + args: [], + ); + } + + /// `Add a memories widget to your homescreen and come back here to customize.` + String get addMemoriesWidgetPrompt { + return Intl.message( + 'Add a memories widget to your homescreen and come back here to customize.', + name: 'addMemoriesWidgetPrompt', + desc: '', + args: [], + ); + } + + /// `Add an album widget to your homescreen and come back here to customize.` + String get addAlbumWidgetPrompt { + return Intl.message( + 'Add an album widget to your homescreen and come back here to customize.', + name: 'addAlbumWidgetPrompt', + desc: '', + args: [], + ); + } + + /// `Add a people widget to your homescreen and come back here to customize.` + String get addPeopleWidgetPrompt { + return Intl.message( + 'Add a people widget to your homescreen and come back here to customize.', + name: 'addPeopleWidgetPrompt', desc: '', args: [], ); diff --git a/mobile/lib/l10n/intl_ar.arb b/mobile/lib/l10n/intl_ar.arb index 699aaa5a88..2ab97549b1 100644 --- a/mobile/lib/l10n/intl_ar.arb +++ b/mobile/lib/l10n/intl_ar.arb @@ -1,8 +1,8 @@ { "@@locale ": "en", "enterYourEmailAddress": "أدخل عنوان بريدك الإلكتروني", - "accountWelcomeBack": "مرحبًا مجددًا!", - "emailAlreadyRegistered": "البريد الإلكتروني مُسَجَّل من قبل.", + "accountWelcomeBack": "أهلاً بعودتك!", + "emailAlreadyRegistered": "البريد الإلكتروني مُسجل من قبل.", "emailNotRegistered": "البريد الإلكتروني غير مسجل.", "email": "البريد الإلكتروني", "cancel": "إلغاء", @@ -17,7 +17,7 @@ "confirmDeletePrompt": "نعم، أرغب في حذف هذا الحساب وبياناته نهائيًا من جميع التطبيقات.", "confirmAccountDeletion": "تأكيد حذف الحساب", "deleteAccountPermanentlyButton": "حذف الحساب نهائيًا", - "yourAccountHasBeenDeleted": "تم حذف حسابك بنجاح.", + "yourAccountHasBeenDeleted": "تم حذف حسابك بنجاح", "selectReason": "اختر سببًا", "deleteReason1": "تفتقر إلى مِيزة أساسية أحتاج إليها", "deleteReason2": "التطبيق أو مِيزة معينة لا تعمل كما هو متوقع", @@ -70,7 +70,7 @@ "changePasswordTitle": "تغيير كلمة المرور", "resetPasswordTitle": "إعادة تعيين كلمة المرور", "encryptionKeys": "مفاتيح التشفير", - "passwordWarning": "نحن لا نخزن كلمة المرور هذه، لذا إذا نسيتها، لا يمكننا المساعدة في فك تشفير بياناتك.", + "passwordWarning": "نحن لا نقوم بتخزين كلمة المرور هذه، لذا إذا نسيتها، لا يمكننا فك تشفير بياناتك", "enterPasswordToEncrypt": "أدخل كلمة مرور يمكننا استخدامها لتشفير بياناتك", "enterNewPasswordToEncrypt": "أدخل كلمة مرور جديدة يمكننا استخدامها لتشفير بياناتك", "weakStrength": "ضعيفة", @@ -88,7 +88,7 @@ }, "message": "Password Strength: {passwordStrengthText}" }, - "passwordChangedSuccessfully": "تم تغيير كلمة المرور بنجاح.", + "passwordChangedSuccessfully": "تم تغيير كلمة المرور بنجاح", "generatingEncryptionKeys": "جارٍ إنشاء مفاتيح التشفير...", "pleaseWait": "يرجى الانتظار...", "continueLabel": "متابعة", @@ -721,6 +721,7 @@ "type": "text" }, "backupFailed": "فشل النسخ الاحتياطي", + "sorryBackupFailedDesc": "عذرًا، لم نتمكن من عمل نسخة احتياطية لهذا الملف الآن، سنعيد المحاولة لاحقًا.", "couldNotBackUpTryLater": "لم نتمكن من نسخ بياناتك احتياطيًا.\nسنحاول مرة أخرى لاحقًا.", "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "يمكن لـ Ente تشفير وحفظ الملفات فقط إذا منحت الإذن بالوصول إليها.", "pleaseGrantPermissions": "يرجى منح الأذونات.", @@ -1029,6 +1030,7 @@ "didYouKnow": "هل تعلم؟", "loadingMessage": "جارٍ تحميل صورك...", "loadMessage1": "يمكنك مشاركة اشتراكك مع عائلتك.", + "loadMessage2": "لقد حفظنا أكثر من 200 مليون ذكرى حتى الآن.", "loadMessage3": "نحتفظ بـ 3 نسخ من بياناتك، إحداها في ملجأ للطوارئ تحت الأرض.", "loadMessage4": "جميع تطبيقاتنا مفتوحة المصدر.", "loadMessage5": "تم تدقيق شفرتنا المصدرية والتشفير الخاص بنا خارجيًا.", @@ -1279,6 +1281,8 @@ "createCollaborativeLink": "إنشاء رابط تعاوني", "search": "بحث", "enterPersonName": "أدخل اسم الشخص", + "editEmailAlreadyLinked": "هذا البريد الإلكتروني مرتبط مسبقاً بـ {name}.", + "viewPersonToUnlink": "عرض {name} لإلغاء الربط", "enterName": "أدخل الاسم", "savePerson": "حفظ الشخص", "editPerson": "تعديل الشخص", @@ -1658,7 +1662,7 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "بث الفيديو", + "videoStreaming": "مقاطع فيديو قابلة للبث", "processingVideos": "معالجة مقاطع الفيديو", "streamDetails": "تفاصيل البث", "processing": "المعالجة", diff --git a/mobile/lib/l10n/intl_da.arb b/mobile/lib/l10n/intl_da.arb index 7870608a78..dfd9dee37f 100644 --- a/mobile/lib/l10n/intl_da.arb +++ b/mobile/lib/l10n/intl_da.arb @@ -207,7 +207,7 @@ "after1Month": "Efter 1 måned", "after1Year": "Efter 1 år", "manageParticipants": "Administrer", - "albumParticipantsCount": "{count, plural, one {}=0 {Ingen Deltagere} =1 {1 Deltager} other {{count} Deltagere}}", + "albumParticipantsCount": "{count, plural, =0 {Ingen Deltagere} =1 {1 Deltager} other {{count} Deltagere}}", "@albumParticipantsCount": { "placeholders": { "count": { diff --git a/mobile/lib/l10n/intl_de.arb b/mobile/lib/l10n/intl_de.arb index fb9caf8220..205cbc65ec 100644 --- a/mobile/lib/l10n/intl_de.arb +++ b/mobile/lib/l10n/intl_de.arb @@ -1,6 +1,7 @@ { "@@locale ": "en", "enterYourEmailAddress": "Gib deine E-Mail-Adresse ein", + "enterYourNewEmailAddress": "Gib Deine neue E-Mail-Adresse ein", "accountWelcomeBack": "Willkommen zurück!", "emailAlreadyRegistered": "E-Mail ist bereits registriert.", "emailNotRegistered": "E-Mail nicht registriert.", @@ -1281,6 +1282,8 @@ "createCollaborativeLink": "Gemeinschaftlichen Link erstellen", "search": "Suche", "enterPersonName": "Namen der Person eingeben", + "editEmailAlreadyLinked": "Diese E-Mail ist bereits verknüpft mit {name}.", + "viewPersonToUnlink": "{name} zum Entfernen des Links anzeigen", "enterName": "Name eingeben", "savePerson": "Person speichern", "editPerson": "Person bearbeiten", @@ -1411,7 +1414,7 @@ }, "description": "Number of viewers that were successfully added to an album." }, - "collaboratorsSuccessfullyAdded": "{count, plural, one {}=0 {0 Mitarbeiter hinzugefügt} =1 {1 Mitarbeiter hinzugefügt} other {{count} Mitarbeiter hinzugefügt}}", + "collaboratorsSuccessfullyAdded": "{count, plural, =0 {0 Mitarbeiter hinzugefügt} =1 {1 Mitarbeiter hinzugefügt} other {{count} Mitarbeiter hinzugefügt}}", "@collaboratorsSuccessfullyAdded": { "placeholders": { "count": { @@ -1660,7 +1663,7 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Video-Streaming", + "videoStreaming": "Streambare Videos", "processingVideos": "Verarbeite Videos", "streamDetails": "Stream-Details", "processing": "In Bearbeitung", @@ -1737,5 +1740,10 @@ "cLFamilyPlanDesc": "Du kannst jetzt festlegen, wie viel Speicherplatz deine Familienmitglieder nutzen können.", "cLBulkEdit": "Massenbearbeitung von Datumsangaben", "cLBulkEditDesc": "Du kannst jetzt mehrere Fotos auswählen, und das Datum/Uhrzeit für alle mit einer Aktion ändern. Das Verschieben von Daten wird auch unterstützt.", - "curatedMemories": "Ausgewählte Erinnerungen" + "curatedMemories": "Ausgewählte Erinnerungen", + "onThisDay": "An diesem Tag", + "deleteMultipleAlbumDialog": "Sollen die Fotos (und Videos) aus diesen {count} Alben auch aus allen anderen Alben gelöscht werden, in denen sie enthalten sind?", + "addParticipants": "Teilnehmer hinzufügen", + "selectedAlbums": "{count} ausgewählt", + "actionNotSupportedOnFavouritesAlbum": "Aktion für das Favoritenalbum nicht unterstützt" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb index d5207f0d10..c6711a6996 100644 --- a/mobile/lib/l10n/intl_en.arb +++ b/mobile/lib/l10n/intl_en.arb @@ -1,6 +1,7 @@ { "@@locale ": "en", "enterYourEmailAddress": "Enter your email address", + "enterYourNewEmailAddress": "Enter your new email address", "accountWelcomeBack": "Welcome back!", "emailAlreadyRegistered": "Email already registered.", "emailNotRegistered": "Email not registered.", @@ -1281,6 +1282,8 @@ "createCollaborativeLink": "Create collaborative link", "search": "Search", "enterPersonName": "Enter person name", + "editEmailAlreadyLinked": "This email is already linked to {name}.", + "viewPersonToUnlink": "View {name} to unlink", "enterName": "Enter name", "savePerson": "Save person", "editPerson": "Edit person", @@ -1660,7 +1663,7 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Generate streamable video", + "videoStreaming": "Streamable videos", "processingVideos": "Processing videos", "streamDetails": "Stream details", "processing": "Processing", @@ -1727,15 +1730,26 @@ "onTheRoad": "On the road again", "food": "Culinary delight", "pets": "Furry companions", - "cLIcon": "New Icon", - "cLIconDesc": "Finally, a new app icon, that we think best represents our work. We've also added an icon-switcher so you can continue using the old icon.", - "cLMemories": "Memories", - "cLMemoriesDesc": "Rediscover your special moments - spotlight on your favorite people, your trips and holidays, your best clicks, and much more. Turn on machine learning, tag yourself and name your friends for the best experience.", - "cLWidgets": "Widgets", - "cLWidgetsDesc": "Home screen widgets that are integrated with memories are now available. They will show your special moments without opening the app.", - "cLFamilyPlan": "Family Plan Limits", - "cLFamilyPlanDesc": "You can now set limits on how much storage your family members can use.", - "cLBulkEdit": "Bulk Edit dates", - "cLBulkEditDesc": "You can now select multiple photos, and edit date/time for all of them with one quick action. Shifting dates is also supported.", - "curatedMemories": "Curated memories" + "curatedMemories": "Curated memories", + "widgets": "Widgets", + "memories": "Memories", + "peopleWidgetDesc": "Select the people you wish to see on your homescreen.", + "albumsWidgetDesc": "Select the albums you wish to see on your homescreen.", + "memoriesWidgetDesc": "Select the kind of memories you wish to see on your homescreen.", + "smartMemories": "Smart memories", + "pastYearsMemories": "Past years' memories", + "deleteMultipleAlbumDialog": "Also delete the photos (and videos) present in these {count} albums from all other albums they are part of?", + "addParticipants": "Add participants", + "selectedAlbums": "{count} selected", + "actionNotSupportedOnFavouritesAlbum": "Action not supported on Favourites album", + "onThisDayMemories": "On this day memories", + "onThisDay": "On this day", + "lookBackOnYourMemories": "Look back on your memories 🌄", + "newPhotosEmoji": " new 📸", + "sorryWeHadToPauseYourBackups": "Sorry, we had to pause your backups", + "clickToInstallOurBestVersionYet": "Click to install our best version yet", + "onThisDayNotificationExplanation": "Receive reminders about memories from this day in previous years.", + "addMemoriesWidgetPrompt": "Add a memories widget to your homescreen and come back here to customize.", + "addAlbumWidgetPrompt": "Add an album widget to your homescreen and come back here to customize.", + "addPeopleWidgetPrompt": "Add a people widget to your homescreen and come back here to customize." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_es.arb b/mobile/lib/l10n/intl_es.arb index 024ebc3513..f5348065ba 100644 --- a/mobile/lib/l10n/intl_es.arb +++ b/mobile/lib/l10n/intl_es.arb @@ -1658,7 +1658,6 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Transmisión de vídeo", "processingVideos": "Procesando vídeos", "streamDetails": "Detalles de la transmisión", "processing": "Procesando", diff --git a/mobile/lib/l10n/intl_fr.arb b/mobile/lib/l10n/intl_fr.arb index bedd75a2f9..3f4dc5004c 100644 --- a/mobile/lib/l10n/intl_fr.arb +++ b/mobile/lib/l10n/intl_fr.arb @@ -1,6 +1,7 @@ { "@@locale ": "en", "enterYourEmailAddress": "Entrez votre adresse e-mail", + "enterYourNewEmailAddress": "Entrez votre nouvelle adresse e-mail", "accountWelcomeBack": "Bon retour parmi nous !", "emailAlreadyRegistered": "Email déjà enregistré.", "emailNotRegistered": "E-mail non enregistré.", @@ -371,6 +372,21 @@ "deleteFromBoth": "Supprimer des deux", "newAlbum": "Nouvel album", "albums": "Albums", + "memoryCount": "{count, plural, =0{aucun souvenir} one{{formattedCount} souvenir} other{{formattedCount} souvenirs}}", + "@memoryCount": { + "description": "The text to display the number of memories", + "type": "text", + "placeholders": { + "count": { + "example": "1", + "type": "int" + }, + "formattedCount": { + "type": "String", + "example": "11.513, 11,511" + } + } + }, "selectedPhotos": "{count} sélectionné(s)", "@selectedPhotos": { "description": "Display the number of selected photos", @@ -706,6 +722,7 @@ "type": "text" }, "backupFailed": "Échec de la sauvegarde", + "sorryBackupFailedDesc": "Désolé, nous n'avons pas pu sauvegarder ce fichier maintenant, nous allons réessayer plus tard.", "couldNotBackUpTryLater": "Nous n'avons pas pu sauvegarder vos données.\nNous allons réessayer plus tard.", "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente peut chiffrer et conserver des fichiers que si vous leur accordez l'accès", "pleaseGrantPermissions": "Veuillez accorder la permission", @@ -874,6 +891,7 @@ "authToViewYourMemories": "Authentifiez-vous pour voir vos souvenirs", "unlock": "Déverrouiller", "freeUpSpace": "Libérer de l'espace", + "freeUpSpaceSaving": "{count, plural, one {}=1 {Il peut être supprimé de l'appareil pour libérer {formattedSize}} other {Ils peuvent être supprimés de l'appareil pour libérer {formattedSize}}}", "filesBackedUpInAlbum": "{count, plural, one {1 fichier dans cet album a été sauvegardé en toute sécurité} other {{formattedNumber} fichiers dans cet album ont été sauvegardés en toute sécurité}}", "@filesBackedUpInAlbum": { "description": "Text to tell user how many files have been backed up in the album", @@ -904,6 +922,18 @@ } } }, + "@freeUpSpaceSaving": { + "description": "Text to tell user how much space they can free up by deleting items from the device" + }, + "freeUpAccessPostDelete": "Vous pouvez toujours {count, plural, one {}=1 {l'} other {les}} accéder sur Ente tant que vous avez un abonnement actif", + "@freeUpAccessPostDelete": { + "placeholders": { + "count": { + "example": "1", + "type": "int" + } + } + }, "freeUpAmount": "Libérer {sizeInMBorGB}", "thisEmailIsAlreadyInUse": "Cette adresse mail est déjà utilisé", "incorrectCode": "Code non valide", @@ -993,6 +1023,7 @@ "didYouKnow": "Le savais-tu ?", "loadingMessage": "Chargement de vos photos...", "loadMessage1": "Vous pouvez partager votre abonnement avec votre famille", + "loadMessage2": "Nous avons préservé plus de 200 millions de souvenirs jusqu'à présent", "loadMessage3": "Nous conservons 3 copies de vos données, l'une dans un abri anti-atomique", "loadMessage4": "Toutes nos applications sont open source", "loadMessage5": "Notre code source et notre cryptographie ont été audités en externe", @@ -1230,6 +1261,8 @@ "description": "Subtitle to indicate that the user can find people quickly by name" }, "findPeopleByName": "Trouver des personnes rapidement par leur nom", + "addViewers": "{count, plural, one {}=0 {Ajouter un spectateur} =1 {Ajouter une spectateur} other {Ajouter des spectateurs}}", + "addCollaborators": "{count, plural, one {}=0 {Ajouter un collaborateur} =1 {Ajouter un collaborateur} other {Ajouter des collaborateurs}}", "longPressAnEmailToVerifyEndToEndEncryption": "Appuyez longuement sur un email pour vérifier le chiffrement de bout en bout.", "developerSettingsWarning": "Êtes-vous sûr de vouloir modifier les paramètres du développeur ?", "developerSettings": "Paramètres du développeur", @@ -1241,6 +1274,8 @@ "createCollaborativeLink": "Créer un lien collaboratif", "search": "Rechercher", "enterPersonName": "Entrez le nom d'une personne", + "editEmailAlreadyLinked": "Cet e-mail est déjà lié à {name}.", + "viewPersonToUnlink": "Voir {name} pour délier", "enterName": "Saisir un nom", "savePerson": "Enregistrer la personne", "editPerson": "Modifier la personne", @@ -1361,6 +1396,16 @@ "enableMachineLearningBanner": "Activer l'apprentissage automatique pour la reconnaissance des visages et la recherche magique", "searchDiscoverEmptySection": "Les images seront affichées ici une fois le traitement terminé", "searchPersonsEmptySection": "Les personnes seront affichées ici une fois le traitement terminé", + "viewersSuccessfullyAdded": "{count, plural, one {}=0 {0 spectateur ajouté} =1 {Un spectateur ajouté} other {{count} spectateurs ajoutés}}", + "@viewersSuccessfullyAdded": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + }, + "description": "Number of viewers that were successfully added to an album." + }, "collaboratorsSuccessfullyAdded": "{count, plural, =0 {0 collaborateur ajouté} =1 {1 collaborateur ajouté} other {{count} collaborateurs ajoutés}}", "@collaboratorsSuccessfullyAdded": { "placeholders": { @@ -1436,6 +1481,15 @@ }, "currentlyRunning": "en cours d'exécution", "ignored": "ignoré", + "photosCount": "{count, plural, =0 {0 photo} =1 {1 photo} other {{count} photos}}", + "@photosCount": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + } + }, "file": "Fichier", "searchSectionsLengthMismatch": "Incompatibilité de longueur des sections : {snapshotLength} != {searchLength}", "@searchSectionsLengthMismatch": { @@ -1601,7 +1655,7 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Streaming vidéo", + "videoStreaming": "Vidéos diffusables", "processingVideos": "Traitement des vidéos", "streamDetails": "Détails du stream", "processing": "Traitement en cours", @@ -1639,7 +1693,7 @@ "selectedItemsWillBeRemovedFromThisPerson": "Les éléments sélectionnés seront retirés de cette personne, mais pas supprimés de votre bibliothèque.", "throughTheYears": "{dateFormat} au fil des années", "thisWeekThroughTheYears": "Cette semaine au fil des années", - "thisWeekXYearsAgo": "{count, plural, one {}=1 {Cette semaine, {count} il y a l'année} other {Cette semaine, {count} il y a des années}}", + "thisWeekXYearsAgo": "{count, plural, =1 {Cette semaine, {count} il y a l'année} other {Cette semaine, {count} il y a des années}}", "youAndThem": "Vous et {name}", "admiringThem": "Admirant {name}", "embracingThem": "Embrasse {name}", @@ -1678,5 +1732,10 @@ "cLFamilyPlanDesc": "Vous pouvez maintenant fixer des limites sur la quantité de stockage que les membres de votre famille peuvent utiliser.", "cLBulkEdit": "Dates de modification multiples", "cLBulkEditDesc": "Vous pouvez maintenant sélectionner plusieurs photos et modifier la date/heure pour toutes celles-ci, en une seule action rapide. Les dates de décalage sont également prises en charge.", - "curatedMemories": "Souvenirs conservés" + "curatedMemories": "Souvenirs conservés", + "onThisDay": "Ce jour-ci", + "deleteMultipleAlbumDialog": "Supprimer également les photos (et les vidéos) présentes dans ces {count} albums de tous les autres albums dont ils font partie ?", + "addParticipants": "Ajouter des participants", + "selectedAlbums": "{count} sélectionné(s)", + "actionNotSupportedOnFavouritesAlbum": "Action non prise en charge sur l'album des Favoris" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_it.arb b/mobile/lib/l10n/intl_it.arb index f8c450a466..7c718f6ab8 100644 --- a/mobile/lib/l10n/intl_it.arb +++ b/mobile/lib/l10n/intl_it.arb @@ -992,6 +992,7 @@ "didYouKnow": "Lo sapevi che?", "loadingMessage": "Caricando le tue foto...", "loadMessage1": "Puoi condividere il tuo abbonamento con la tua famiglia", + "loadMessage2": "Finora abbiamo conservato oltre 200 milioni di ricordi", "loadMessage3": "Teniamo 3 copie dei tuoi dati, uno in un rifugio sotterraneo antiatomico", "loadMessage4": "Tutte le nostre app sono open source", "loadMessage5": "Il nostro codice sorgente e la crittografia hanno ricevuto audit esterni", @@ -1497,6 +1498,15 @@ "useDifferentPlayerInfo": "Hai problemi a riprodurre questo video? Premi a lungo qui per provare un altro lettore.", "hideSharedItemsFromHomeGallery": "Nascondi gli elementi condivisi dalla galleria principale", "gallery": "Galleria", + "emailDoesNotHaveEnteAccount": "{email} non ha un account Ente.", + "@emailDoesNotHaveEnteAccount": { + "description": "Shown when email doesn't have an Ente account", + "placeholders": { + "email": { + "type": "String" + } + } + }, "accountOwnerPersonAppbarTitle": "{title} (Io)", "@accountOwnerPersonAppbarTitle": { "description": "Title of appbar for account owner person", @@ -1537,5 +1547,6 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "processingVideos": "Elaborando video" + "processingVideos": "Elaborando video", + "joinAlbumConfirmationDialogBody": "Unirsi a un album renderà visibile la tua email ai suoi partecipanti." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_ja.arb b/mobile/lib/l10n/intl_ja.arb index 73fb2dd257..e4082bd51b 100644 --- a/mobile/lib/l10n/intl_ja.arb +++ b/mobile/lib/l10n/intl_ja.arb @@ -1601,7 +1601,6 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "動画ストリーミング", "processingVideos": "動画を処理中", "streamDetails": "動画の詳細", "processing": "処理中", diff --git a/mobile/lib/l10n/intl_lt.arb b/mobile/lib/l10n/intl_lt.arb index 9f9dc2231a..5b71c6fba1 100644 --- a/mobile/lib/l10n/intl_lt.arb +++ b/mobile/lib/l10n/intl_lt.arb @@ -371,7 +371,7 @@ "deleteFromBoth": "Ištrinti iš abiejų", "newAlbum": "Naujas albumas", "albums": "Albumai", - "memoryCount": "{count, plural, =0 {nėra prisiminimų} one {{formattedCount} prisiminimas} few {{formattedCount} prisiminimai} many {{formattedCount} prisiminimo} other {{formattedCount} prisiminimų}}", + "memoryCount": "{count, plural, =0 {nėra prisiminimų} one {{formattedCount} prisiminimas} other {{formattedCount} prisiminimų}}", "@memoryCount": { "description": "The text to display the number of memories", "type": "text", @@ -458,8 +458,8 @@ "selectAll": "Pasirinkti viską", "skip": "Praleisti", "updatingFolderSelection": "Atnaujinamas aplankų pasirinkimas...", - "itemCount": "{count, plural, one{{count} elementas} few {{count} elementai} many {{count} elemento} other{{count} elementų}}", - "deleteItemCount": "{count, plural, =1 {Ištrinti {count} elementą} few {Ištrinti {count} elementus} many {Ištrinti {count} elemento} other {Ištrinti {count} elementų}}", + "itemCount": "{count, plural, one{{count} elementas} other{{count} elementų}}", + "deleteItemCount": "{count, plural, =1 {Ištrinti {count} elementą} other {Ištrinti {count} elementų}}", "duplicateItemsGroup": "{count} failai (-ų), kiekvienas {formattedSize}", "@duplicateItemsGroup": { "description": "Display the number of duplicate files and their size", @@ -476,7 +476,7 @@ } }, "showMemories": "Rodyti prisiminimus", - "yearsAgo": "{count, plural, one{prieš {count} metus} few {prieš {count} metus} many {prieš {count} metų} other{prieš {count} metų}}", + "yearsAgo": "{count, plural, one{prieš {count} metus} other{prieš {count} metų}}", "backupSettings": "Atsarginės kopijos nustatymai", "backupStatus": "Atsarginės kopijos būsena", "backupStatusDescription": "Čia bus rodomi elementai, kurių atsarginės kopijos buvo sukurtos.", @@ -542,7 +542,7 @@ }, "remindToEmptyEnteTrash": "Taip pat ištuštinkite šiukšlinę, kad gautumėte laisvos vietos.", "sparkleSuccess": "✨ Sėkmė", - "duplicateFileCountWithStorageSaved": "Išvalėte {count, plural, one{{count} dubliuojantį failą} few {{count} dubliuojančius failus} many {{count} dubliuojančio failo} other{{count} dubliuojančių failų}}, išsaugodami ({storageSaved}).", + "duplicateFileCountWithStorageSaved": "Išvalėte {count, plural, one{{count} dubliuojantį failą} other{{count} dubliuojančių failų}}, išsaugodami ({storageSaved})", "@duplicateFileCountWithStorageSaved": { "description": "The text to display when the user has successfully cleaned up duplicate files", "type": "text", @@ -693,8 +693,11 @@ "startBackup": "Pradėti kurti atsarginę kopiją", "noPhotosAreBeingBackedUpRightNow": "Šiuo metu nekuriamos atsarginės nuotraukų kopijos", "preserveMore": "Išsaugoti daugiau", + "grantFullAccessPrompt": "Leiskite prieigą prie visų nuotraukų nustatymų programoje.", "allowPermTitle": "Leisti prieigą prie nuotraukų", "allowPermBody": "Iš nustatymų leiskite prieigą prie nuotraukų, kad „Ente“ galėtų rodyti ir kurti atsargines bibliotekos kopijas.", + "openSettings": "Atverti nustatymus", + "selectMorePhotos": "Pasirinkti daugiau nuotraukų", "existingUser": "Esamas naudotojas", "privateBackups": "Privačios atsarginės kopijos", "forYourMemories": "jūsų prisiminimams", @@ -717,8 +720,10 @@ "description": "Button text for raising a support tickets in case of unhandled errors during backup", "type": "text" }, + "backupFailed": "Atsarginė kopija nepavyko", "sorryBackupFailedDesc": "Atsiprašome, šiuo metu negalėjome sukurti atsarginės šio failo kopijos. Bandysime pakartoti vėliau.", "couldNotBackUpTryLater": "Nepavyko sukurti atsarginės duomenų kopijos.\nBandysime pakartotinai vėliau.", + "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "„Ente“ gali užšifruoti ir išsaugoti failus tik tada, jei suteikiate prieigą prie jų.", "pleaseGrantPermissions": "Suteikite leidimus.", "grantPermission": "Suteikti leidimą", "privateSharing": "Privatus bendrinimas", @@ -762,6 +767,10 @@ } }, "permanentlyDelete": "Ištrinti negrįžtamai", + "canOnlyCreateLinkForFilesOwnedByYou": "Galima sukurti nuorodą tik jums priklausantiems failams", + "publicLinkCreated": "Vieša nuoroda sukurta", + "youCanManageYourLinksInTheShareTab": "Nuorodas galite valdyti bendrinimo kortelėje.", + "linkCopiedToClipboard": "Nuoroda nukopijuota į iškarpinę", "restore": "Atkurti", "@restore": { "description": "Display text for an action which triggers a restore of item from trash", @@ -775,26 +784,43 @@ "shareLink": "Bendrinti nuorodą", "createCollage": "Kurti koliažą", "saveCollage": "Išsaugoti koliažą", + "collageSaved": "Koliažas išsaugotas į galeriją", + "collageLayout": "Išdėstymas", "addToEnte": "Pridėti į „Ente“", "addToAlbum": "Pridėti į albumą", "delete": "Ištrinti", "hide": "Slėpti", "share": "Bendrinti", + "unhideToAlbum": "Rodyti į albumą", "restoreToAlbum": "Atkurti į albumą", - "moveItem": "{count, plural, one {Perkelti elementą} few {Perkelti elementus} many {Perkelti elemento} =1 {Perkelti elementą} other {Perkelti elementų}}", + "moveItem": "{count, plural, =1 {Perkelti elementą} other {Perkelti elementų}}", "@moveItem": { "description": "Page title while moving one or more items to an album" }, - "addItem": "{count, plural, one {Pridėti elementą} few {Pridėti elementus} many {Pridėti elemento} =1 {Pridėti elementą} other {Pridėti elementų}}", + "addItem": "{count, plural, =1 {Pridėti elementą} other {Pridėti elementų}}", "@addItem": { "description": "Page title while adding one or more items to album" }, "createOrSelectAlbum": "Kurkite arba pasirinkite albumą", + "selectAlbum": "Pasirinkti albumą", "searchByAlbumNameHint": "Albumo pavadinimas", "albumTitle": "Albumo pavadinimas", "enterAlbumName": "Įveskite albumo pavadinimą", "restoringFiles": "Atkuriami failai...", + "movingFilesToAlbum": "Perkeliami failai į albumą...", + "unhidingFilesToAlbum": "Rodomi failai į albumą", + "canNotUploadToAlbumsOwnedByOthers": "Negalima įkelti į kitiems priklausančius albumus", + "uploadingFilesToAlbum": "Įkeliami failai į albumą...", + "addedSuccessfullyTo": "Sėkmingai įtraukta į „{albumName}“", + "movedSuccessfullyTo": "Sėkmingai perkelta į „{albumName}“", + "thisAlbumAlreadyHDACollaborativeLink": "Šis albumas jau turi bendradarbiavimo nuorodą.", + "collaborativeLinkCreatedFor": "Bendradarbiavimo nuoroda sukurta albumui „{albumName}“", + "askYourLovedOnesToShare": "Paprašykite savo artimuosius bendrinti", + "invite": "Kviesti", + "shareYourFirstAlbum": "Bendrinkite savo pirmąjį albumą", + "sharedWith": "Bendrinta su {emailIDs}", "sharedWithMe": "Bendrinta su manimi", + "sharedByMe": "Bendrinta manimi", "doubleYourStorage": "Padvigubinkite saugyklą", "referFriendsAnd2xYourPlan": "Rekomenduokite draugams ir 2 kartus padidinkite savo planą", "shareAlbumHint": "Atidarykite albumą ir palieskite bendrinimo mygtuką viršuje dešinėje, kad bendrintumėte.", @@ -810,6 +836,8 @@ } }, "deleteAll": "Ištrinti viską", + "renameAlbum": "Pervadinti albumą", + "convertToAlbum": "Konvertuoti į albumą", "setCover": "Nustatyti viršelį", "@setCover": { "description": "Text to set cover photo for an album" @@ -821,15 +849,26 @@ "leaveSharedAlbum": "Palikti bendrinamą albumą?", "leaveAlbum": "Palikti albumą", "photosAddedByYouWillBeRemovedFromTheAlbum": "Jūsų pridėtos nuotraukos bus pašalintos iš albumo", + "youveNoFilesInThisAlbumThatCanBeDeleted": "Neturite šiame albume failų, kuriuos būtų galima ištrinti.", "youDontHaveAnyArchivedItems": "Neturite jokių archyvuotų elementų.", + "ignoredFolderUploadReason": "Kai kurie šio albumo failai ignoruojami, nes anksčiau buvo ištrinti iš „Ente“.", + "resetIgnoredFiles": "Atkurti ignoruojamus failus", + "deviceFilesAutoUploading": "Į šį įrenginio albumą įtraukti failai bus automatiškai įkelti į „Ente“.", + "turnOnBackupForAutoUpload": "Įjunkite atsarginės kopijos kūrimą, kad automatiškai įkeltumėte į šį įrenginio aplanką įtrauktus failus į „Ente“.", + "noHiddenPhotosOrVideos": "Nėra paslėptų nuotraukų arba vaizdo įrašų", "toHideAPhotoOrVideo": "Kad paslėptumėte nuotrauką ar vaizdo įrašą", "openTheItem": "• Atverkite elementą.", "clickOnTheOverflowMenu": "• Spustelėkite ant perpildymo meniu", "nothingToSeeHere": "Čia nėra nieko, ką pamatyti. 👀", "unarchiveAlbum": "Išarchyvuoti albumą", "archiveAlbum": "Archyvuoti albumą", + "calculating": "Skaičiuojama...", + "pleaseWaitDeletingAlbum": "Palaukite. Ištrinamas albumas", "searchByExamples": "• Albumų pavadinimai (pvz., „Fotoaparatas“)\n• Failų tipai (pvz., „Vaizdo įrašai“, „.gif“)\n• Metai ir mėnesiai (pvz., „2022“, „sausis“)\n• Šventės (pvz., „Kalėdos“)\n• Nuotraukų aprašymai (pvz., „#džiaugsmas“)", + "youCanTrySearchingForADifferentQuery": "Galite pabandyti ieškoti pagal kitą užklausą.", "noResultsFound": "Rezultatų nerasta.", + "addedBy": "Įtraukė {emailOrName}", + "loadingExifData": "Įkeliami EXIF duomenys...", "viewAllExifData": "Peržiūrėti visus EXIF duomenis", "noExifData": "Nėra EXIF duomenų", "thisImageHasNoExifData": "Šis vaizdas neturi Exif duomenų", @@ -849,9 +888,35 @@ "deduplicateFiles": "Atdubliuoti failus", "deselectAll": "Naikinti visų pasirinkimą", "reviewDeduplicateItems": "Peržiūrėkite ir ištrinkite elementus, kurie, jūsų manymu, yra dublikatai.", + "clubByCaptureTime": "Grupuoti pagal užfiksavimo laiką", + "clubByFileName": "Grupuoti pagal failo pavadinimą", + "count": "Skaičių", + "totalSize": "Bendrą dydį", + "longpressOnAnItemToViewInFullscreen": "Ilgai paspauskite elementą, kad peržiūrėtumėte per visą ekraną", + "decryptingVideo": "Iššifruojamas vaizdo įrašas...", + "authToViewYourMemories": "Nustatykite tapatybę, kad peržiūrėtumėte savo prisiminimus", "unlock": "Atrakinti", "freeUpSpace": "Atlaisvinti vietos", - "freeUpAccessPostDelete": "Vis dar galite pasiekti {count, plural, one {jį} few {juos} many {juos} =1 {jį} other {jų}} platformoje „Ente“, kol turite aktyvų prenumeratą.", + "freeUpSpaceSaving": "{count, plural, =1 {Jį galima ištrinti iš įrenginio, kad atlaisvintų {formattedSize}} other {Jų galima ištrinti iš įrenginio, kad atlaisvintų {formattedSize}}}", + "filesBackedUpInAlbum": "{count, plural, one {{formattedNumber} failas šiame albume saugiai sukurta atsarginė kopija} few {{formattedNumber} failai šiame albume saugiai sukurtos atsarginės kopijos} many {{formattedNumber} failo šiame albume saugiai sukurtos atsargines kopijos} other {{formattedNumber} failų šiame albume saugiai sukurta atsarginė kopija}}.", + "@filesBackedUpInAlbum": { + "description": "Text to tell user how many files have been backed up in the album", + "placeholders": { + "count": { + "example": "1", + "type": "int" + }, + "formattedNumber": { + "content": "{formattedNumber}", + "example": "1,000", + "type": "String" + } + } + }, + "@freeUpSpaceSaving": { + "description": "Text to tell user how much space they can free up by deleting items from the device" + }, + "freeUpAccessPostDelete": "Vis dar galite pasiekti {count, plural, =1 {jį} other {jų}} platformoje „Ente“, kol turite aktyvų prenumeratą.", "@freeUpAccessPostDelete": { "placeholders": { "count": { @@ -881,6 +946,16 @@ "encryptingBackup": "Šifruojama atsarginė kopija...", "syncStopped": "Sinchronizavimas sustabdytas", "syncProgress": "{completed} / {total} išsaugomi prisiminimai", + "uploadingMultipleMemories": "Išsaugomi {count} prisiminimai...", + "@uploadingMultipleMemories": { + "description": "Text to tell user how many memories are being preserved", + "placeholders": { + "count": { + "type": "String" + } + } + }, + "uploadingSingleMemory": "Išsaugomas prisiminimas...", "@syncProgress": { "description": "Text to tell user how many memories have been preserved", "placeholders": { @@ -1053,7 +1128,7 @@ "searchHint3": "Albumai, failų pavadinimai ir tipai", "searchHint4": "Vietovė", "searchHint5": "Jau netrukus: veidų ir magiškos paieškos ✨", - "searchResultCount": "{count, plural, one{Rastas {count} rezultatas} few {Rasti {count} rezultatai} many {Rasta {count} rezultato} other{Rasta {count} rezultatų}}", + "searchResultCount": "{count, plural, one{Rastas {count} rezultatas} other{Rasta {count} rezultatų}}", "@searchResultCount": { "description": "Text to tell user how many results were found for their search query", "placeholders": { @@ -1102,8 +1177,8 @@ "description": "Subtitle to indicate that the user can find people quickly by name" }, "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} =0 {Pridėti žiūrėtojų} =1 {Pridėti žiūrėtoją} other {Pridėti žiūrėtojų}}", - "addCollaborators": "{count, plural, one {Pridėti bendradarbį} few {Pridėti bendradarbius} many {Pridėti bendradarbio} =0 {Pridėti bendradarbių} =1 {Pridėti bendradarbį} other {Pridėti bendradarbių}}", + "addViewers": "{count, plural, =0 {Pridėti žiūrėtojų} =1 {Pridėti žiūrėtoją} other {Pridėti žiūrėtojų}}", + "addCollaborators": "{count, plural, =0 {Pridėti bendradarbių} =1 {Pridėti bendradarbį} 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", @@ -1232,16 +1307,6 @@ "enableMachineLearningBanner": "Įjunkite mašininį mokymąsi magiškai paieškai ir veidų atpažinimui", "searchDiscoverEmptySection": "Vaizdai bus rodomi čia, kai bus užbaigtas apdorojimas ir sinchronizavimas.", "searchPersonsEmptySection": "Asmenys bus rodomi čia, kai bus užbaigtas apdorojimas ir sinchronizavimas.", - "viewersSuccessfullyAdded": "{count, plural, one {Pridėtas {count} žiūrėtojas} few {Pridėti {count} žiūrėtojai} many {Pridėta {count} žiūrėtojo} =0 {Pridėta 0 žiūrėtojų} =1 {Pridėtas 1 žiūrėtojas} other {Pridėta {count} žiūrėtojų}}", - "@viewersSuccessfullyAdded": { - "placeholders": { - "count": { - "type": "int", - "example": "2" - } - }, - "description": "Number of viewers that were successfully added to an album." - }, "collaboratorsSuccessfullyAdded": "{count, plural, =0 {Pridėta 0 bendradarbių} =1 {Pridėtas 1 bendradarbis} other {Pridėta {count} bendradarbių}}", "@collaboratorsSuccessfullyAdded": { "placeholders": { @@ -1317,15 +1382,6 @@ }, "currentlyRunning": "šiuo metu vykdoma", "ignored": "ignoruota", - "photosCount": "{count, plural, one {{count} nuotrauka} few {{count} nuotraukos} many {{count} nuotraukos} =0 {0 nuotraukų} =1 {1 nuotrauka} other {{count} nuotraukų}}", - "@photosCount": { - "placeholders": { - "count": { - "type": "int", - "example": "2" - } - } - }, "file": "Failas", "searchSectionsLengthMismatch": "Sekcijų ilgio neatitikimas: {snapshotLength} != {searchLength}", "@searchSectionsLengthMismatch": { @@ -1475,7 +1531,7 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Vaizdo įrašų srautinis perdavimas", + "videoStreaming": "Srautiniai vaizdo įrašai", "processingVideos": "Apdorojami vaizdo įrašai", "streamDetails": "Srautinio perdavimo išsami informacija", "processing": "Apdorojama", @@ -1499,7 +1555,7 @@ "moveSelectedPhotosToOneDate": "Perkelti pasirinktas nuotraukas į vieną datą", "shiftDatesAndTime": "Pastumti datas ir laiką", "photosKeepRelativeTimeDifference": "Nuotraukos išlaiko santykinį laiko skirtumą", - "photocountPhotos": "{count, plural, one {{count} nuotrauka} few {{count} nuotraukos} many {{count} nuotraukos} =0 {Nėra nuotraukų} =1 {1 nuotrauka} other {{count} nuotraukų}}", + "photocountPhotos": "{count, plural, =0 {Nėra nuotraukų} =1 {1 nuotrauka} other {{count} nuotraukų}}", "@photocountPhotos": { "placeholders": { "count": { @@ -1513,7 +1569,7 @@ "selectedItemsWillBeRemovedFromThisPerson": "Pasirinkti elementai bus pašalinti iš šio asmens, bet nebus ištrinti iš jūsų bibliotekos.", "throughTheYears": "{dateFormat} per metus", "thisWeekThroughTheYears": "Ši savaitė per metus", - "thisWeekXYearsAgo": "{count, plural, one {Šią savaitę, prieš {count} metus} few {Šią savaitę, prieš {count} metus} many {Šią savaitę, prieš {count} metų}=1 {Šią savaitę, prieš {count} metus} other {Šią savaitę, prieš {count} metų}}", + "thisWeekXYearsAgo": "{count, plural, =1 {Šią savaitę, prieš {count} metus} other {Šią savaitę, prieš {count} metų}}", "youAndThem": "Jūs ir {name}", "admiringThem": "Žavisi {name}", "backgroundWithThem": "Gražūs vaizdai su {name}", @@ -1538,5 +1594,6 @@ "cLFamilyPlanDesc": "Dabar galite nustatyti ribas, kiek saugyklos gali naudoti jūsų šeimos nariai.", "cLBulkEdit": "Masiškai redaguokite datas", "cLBulkEditDesc": "Dabar galite pasirinkti kelias nuotraukas ir vienu sparčiu veiksmu redaguoti visų nuotraukų datą ir laiką. Taip pat palaikomas datų perkėlimas.", - "curatedMemories": "Kuruoti prisiminimai" + "curatedMemories": "Kuruoti prisiminimai", + "onThisDay": "Šią dieną" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_nl.arb b/mobile/lib/l10n/intl_nl.arb index d57e9c117e..2afe314f56 100644 --- a/mobile/lib/l10n/intl_nl.arb +++ b/mobile/lib/l10n/intl_nl.arb @@ -1,6 +1,7 @@ { "@@locale ": "en", "enterYourEmailAddress": "Voer je e-mailadres in", + "enterYourNewEmailAddress": "Voer uw nieuwe e-mailadres in", "accountWelcomeBack": "Welkom terug!", "emailAlreadyRegistered": "E-mail is al geregistreerd.", "emailNotRegistered": "E-mail niet geregistreerd.", @@ -371,6 +372,21 @@ "deleteFromBoth": "Verwijder van beide", "newAlbum": "Nieuw album", "albums": "Albums", + "memoryCount": "{count, plural, =0{geen herinneringen} one{{formattedCount} herinnering} other{{formattedCount} herinneringen}}", + "@memoryCount": { + "description": "The text to display the number of memories", + "type": "text", + "placeholders": { + "count": { + "example": "1", + "type": "int" + }, + "formattedCount": { + "type": "String", + "example": "11.513, 11,511" + } + } + }, "selectedPhotos": "{count} geselecteerd", "@selectedPhotos": { "description": "Display the number of selected photos", @@ -706,6 +722,7 @@ "type": "text" }, "backupFailed": "Back-up mislukt", + "sorryBackupFailedDesc": "Sorry, we kunnen dit bestand nu niet back-uppen, we zullen het later opnieuw proberen.", "couldNotBackUpTryLater": "We konden uw gegevens niet back-uppen.\nWe zullen het later opnieuw proberen.", "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente kan bestanden alleen versleutelen en bewaren als u toegang tot ze geeft", "pleaseGrantPermissions": "Geef alstublieft toestemming", @@ -777,6 +794,14 @@ "share": "Delen", "unhideToAlbum": "Zichtbaar maken in album", "restoreToAlbum": "Terugzetten naar album", + "moveItem": "{count, plural, =1 {Bestand verplaatsen} other {Bestanden verplaatsen}}", + "@moveItem": { + "description": "Page title while moving one or more items to an album" + }, + "addItem": "{count, plural, =1 {Bestand toevoegen} other {Bestanden toevoegen}}", + "@addItem": { + "description": "Page title while adding one or more items to album" + }, "createOrSelectAlbum": "Maak of selecteer album", "selectAlbum": "Album selecteren", "searchByAlbumNameHint": "Albumnaam", @@ -874,6 +899,7 @@ "authToViewYourMemories": "Graag verifiëren om uw herinneringen te bekijken", "unlock": "Ontgrendelen", "freeUpSpace": "Ruimte vrijmaken", + "freeUpSpaceSaving": "{count, plural, =1 {Het kan verwijderd worden van het apparaat om {formattedSize} vrij te maken} other {Ze kunnen verwijderd worden van het apparaat om {formattedSize} vrij te maken}}", "filesBackedUpInAlbum": "{count, plural, one {1 bestand} other {{formattedNumber} bestanden}} in dit album is veilig geback-upt", "@filesBackedUpInAlbum": { "description": "Text to tell user how many files have been backed up in the album", @@ -904,6 +930,18 @@ } } }, + "@freeUpSpaceSaving": { + "description": "Text to tell user how much space they can free up by deleting items from the device" + }, + "freeUpAccessPostDelete": "Je hebt nog steeds toegang tot {count, plural, =1 {het} other {ze}} op Ente zolang je een actief abonnement hebt", + "@freeUpAccessPostDelete": { + "placeholders": { + "count": { + "example": "1", + "type": "int" + } + } + }, "freeUpAmount": "Maak {sizeInMBorGB} vrij", "thisEmailIsAlreadyInUse": "Dit e-mailadres is al in gebruik", "incorrectCode": "Onjuiste code", @@ -993,6 +1031,7 @@ "didYouKnow": "Wist u dat?", "loadingMessage": "Uw foto's laden...", "loadMessage1": "U kunt uw abonnement met uw familie delen", + "loadMessage2": "We hebben tot nu toe meer dan 200 miljoen herinneringen bewaard", "loadMessage3": "We bewaren 3 kopieën van uw bestanden, één in een ondergrondse kernbunker", "loadMessage4": "Al onze apps zijn open source", "loadMessage5": "Onze broncode en cryptografie zijn extern gecontroleerd en geverifieerd", @@ -1230,6 +1269,8 @@ "description": "Subtitle to indicate that the user can find people quickly by name" }, "findPeopleByName": "Mensen snel op naam zoeken", + "addViewers": "{count, plural, =0 {Kijker toevoegen} =1 {Kijker toevoegen} other {Kijkers toevoegen}}", + "addCollaborators": "{count, plural, =0 {Samenwerker toevoegen} =1 {Samenwerker toevoegen} other {Samenwerkers toevoegen}}", "longPressAnEmailToVerifyEndToEndEncryption": "Druk lang op een e-mail om de versleuteling te verifiëren.", "developerSettingsWarning": "Weet je zeker dat je de ontwikkelaarsinstellingen wilt wijzigen?", "developerSettings": "Ontwikkelaarsinstellingen", @@ -1241,6 +1282,8 @@ "createCollaborativeLink": "Maak een gezamenlijke link", "search": "Zoeken", "enterPersonName": "Naam van persoon invoeren", + "editEmailAlreadyLinked": "Dit e-mailadres is al aan {name} gekoppeld.", + "viewPersonToUnlink": "Bekijk {name} om te ontkoppelen", "enterName": "Naam invoeren", "savePerson": "Persoon opslaan", "editPerson": "Persoon bewerken", @@ -1361,6 +1404,16 @@ "enableMachineLearningBanner": "Schakel machine learning in voor magische zoekopdrachten en gezichtsherkenning", "searchDiscoverEmptySection": "Afbeeldingen worden hier getoond zodra verwerking en synchroniseren voltooid is", "searchPersonsEmptySection": "Personen worden hier getoond zodra verwerking en synchroniseren voltooid is", + "viewersSuccessfullyAdded": "{count, plural, =0 {0 kijkers toegevoegd} =1 {1 kijker toegevoegd} other {{count} kijkers toegevoegd}}", + "@viewersSuccessfullyAdded": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + }, + "description": "Number of viewers that were successfully added to an album." + }, "collaboratorsSuccessfullyAdded": "{count, plural, =0 {0 samenwerkers toegevoegd} =1 {1 samenwerker toegevoegd} other {{count} samenwerkers toegevoegd}}", "@collaboratorsSuccessfullyAdded": { "placeholders": { @@ -1436,6 +1489,15 @@ }, "currentlyRunning": "momenteel bezig", "ignored": "genegeerd", + "photosCount": "{count, plural, =0 {0 foto's} =1 {1 foto} other {{count} foto's}}", + "@photosCount": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + } + }, "file": "Bestand", "searchSectionsLengthMismatch": "Lengte van secties komt niet overeen: {snapshotLength} != {searchLength}", "@searchSectionsLengthMismatch": { @@ -1601,7 +1663,7 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Video streamen", + "videoStreaming": "Video's met stream", "processingVideos": "Video's verwerken", "streamDetails": "Stream details", "processing": "Verwerken", @@ -1677,5 +1739,11 @@ "cLFamilyPlan": "Familieplan limieten", "cLFamilyPlanDesc": "Je kunt nu limieten instellen voor hoeveel opslag je familieleden kunnen gebruiken.", "cLBulkEdit": "Bulk datums wijzigen", - "cLBulkEditDesc": "Je kunt nu meerdere foto's selecteren en de datum/tijd van ze allemaal bewerken met één snelle actie. Verschuiven van datums wordt ook ondersteund." + "cLBulkEditDesc": "Je kunt nu meerdere foto's selecteren en de datum/tijd van ze allemaal bewerken met één snelle actie. Verschuiven van datums wordt ook ondersteund.", + "curatedMemories": "Samengestelde herinneringen", + "onThisDay": "Op deze dag", + "deleteMultipleAlbumDialog": "Verwijder de foto's (en video's) van deze {count} albums ook uit alle andere albums waar deze deel van uitmaken?", + "addParticipants": "Voeg deelnemers toe", + "selectedAlbums": "{count} geselecteerd", + "actionNotSupportedOnFavouritesAlbum": "Actie niet ondersteund op Favorieten album" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_no.arb b/mobile/lib/l10n/intl_no.arb index 813e4366af..f6d8a704c3 100644 --- a/mobile/lib/l10n/intl_no.arb +++ b/mobile/lib/l10n/intl_no.arb @@ -1384,7 +1384,7 @@ "enableMachineLearningBanner": "Aktiver maskinlæring for magisk søk og ansiktsgjenkjenning", "searchDiscoverEmptySection": "Bilder vil vises her når behandlingen og synkronisering er fullført", "searchPersonsEmptySection": "Folk vil vises her når behandling og synkronisering er fullført", - "collaboratorsSuccessfullyAdded": "{count, plural, one {}=0 {La til 0 samarbeidspartner} =1 {La til 1 samarbeidspartner} other {Lagt til {count} samarbeidspartnere}}", + "collaboratorsSuccessfullyAdded": "{count, plural, =0 {La til 0 samarbeidspartner} =1 {La til 1 samarbeidspartner} other {Lagt til {count} samarbeidspartnere}}", "@collaboratorsSuccessfullyAdded": { "placeholders": { "count": { @@ -1624,7 +1624,6 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Videostrømming", "processingVideos": "Behandler videoer", "streamDetails": "Strømmedetaljer", "processing": "Behandler", diff --git a/mobile/lib/l10n/intl_pt.arb b/mobile/lib/l10n/intl_pt.arb index 4c8b5f3eba..e88e9351f7 100644 --- a/mobile/lib/l10n/intl_pt.arb +++ b/mobile/lib/l10n/intl_pt.arb @@ -1,61 +1,59 @@ { "@@locale ": "en", - "enterYourEmailAddress": "Insira seu endereço de e-mail", - "accountWelcomeBack": "Bem-vindo(a) de volta!", - "emailAlreadyRegistered": "E-mail já registrado.", - "emailNotRegistered": "E-mail não registrado.", - "email": "E-mail", + "enterYourEmailAddress": "Insira o seu endereço de email", + "accountWelcomeBack": "Bem-vindo de volta!", + "email": "Email", "cancel": "Cancelar", "verify": "Verificar", - "invalidEmailAddress": "Endereço de e-mail inválido", - "enterValidEmail": "Insira um endereço de e-mail válido.", - "deleteAccount": "Excluir conta", - "askDeleteReason": "Por que você quer excluir sua conta?", - "deleteAccountFeedbackPrompt": "Lamentamos você ir. Compartilhe seu feedback para nos ajudar a melhorar.", - "feedback": "Feedback", - "kindlyHelpUsWithThisInformation": "Ajude-nos com esta informação", - "confirmDeletePrompt": "Sim, eu quero permanentemente excluir esta conta e os dados em todos os aplicativos.", - "confirmAccountDeletion": "Confirmar exclusão da conta", + "invalidEmailAddress": "Endereço de email inválido", + "enterValidEmail": "Por favor, insira um endereço de email válido.", + "deleteAccount": "Eliminar conta", + "askDeleteReason": "Qual o principal motivo pelo qual está a eliminar a conta?", + "deleteAccountFeedbackPrompt": "Lamentamos a sua partida. Indique-nos a razão para podermos melhorar o serviço.", + "feedback": "Opinião", + "kindlyHelpUsWithThisInformation": "Por favor, ajude-nos com esta informação", + "confirmDeletePrompt": "Sim, pretendo apagar permanentemente esta conta e os respetivos dados em todas as aplicações.", + "confirmAccountDeletion": "Confirmar eliminação de conta", "deleteAccountPermanentlyButton": "Excluir conta permanentemente", - "yourAccountHasBeenDeleted": "Sua conta foi excluída", - "selectReason": "Diga o motivo", - "deleteReason1": "Está faltando um recurso-chave que eu preciso", - "deleteReason2": "O aplicativo ou um certo recurso não funciona da maneira que eu acredito que deveria funcionar", - "deleteReason3": "Encontrei outro serviço que considero melhor", - "deleteReason4": "Meu motivo não está listado", - "sendEmail": "Enviar e-mail", - "deleteRequestSLAText": "Sua solicitação será revisada em até 72 horas.", - "deleteEmailRequest": "Por favor, envie um e-mail a account-deletion@ente.io do seu endereço de e-mail registrado.", + "yourAccountHasBeenDeleted": "A sua conta foi eliminada", + "selectReason": "Selecionar motivo", + "deleteReason1": "Falta uma funcionalidade-chave de que eu necessito", + "deleteReason2": "O aplicativo ou um determinado recurso não se comportou como era suposto", + "deleteReason3": "Encontrei outro serviço de que gosto mais", + "deleteReason4": "O motivo não está na lista", + "sendEmail": "Enviar email", + "deleteRequestSLAText": "O seu pedido será processado dentro de 72 horas.", + "deleteEmailRequest": "Envie um e-mail para accountt-deletion@ente.io a partir do seu endereço de email registrado.", "entePhotosPerm": "Ente precisa de permissão para preservar suas fotos", - "ok": "OK", + "ok": "Ok", "createAccount": "Criar conta", "createNewAccount": "Criar nova conta", - "password": "Senha", - "confirmPassword": "Confirmar senha", + "password": "Palavra-passe", + "confirmPassword": "Confirmar palavra-passe", "activeSessions": "Sessões ativas", - "oops": "Ops", - "somethingWentWrongPleaseTryAgain": "Algo deu errado. Tente outra vez", - "thisWillLogYouOutOfThisDevice": "Isso fará você sair deste dispositivo!", - "thisWillLogYouOutOfTheFollowingDevice": "Isso fará você sair do dispositivo a seguir:", - "terminateSession": "Sair?", - "terminate": "Encerrar", + "oops": "Oops", + "somethingWentWrongPleaseTryAgain": "Ocorreu um erro. Tente novamente", + "thisWillLogYouOutOfThisDevice": "Irá desconectar a sua conta do seu dispositivo!", + "thisWillLogYouOutOfTheFollowingDevice": "Irá desconectar a sua conta do seguinte dispositivo:", + "terminateSession": "Terminar sessão?", + "terminate": "Terminar", "thisDevice": "Este dispositivo", "recoverButton": "Recuperar", - "recoverySuccessful": "Recuperação com sucesso!", - "decrypting": "Descriptografando...", + "recoverySuccessful": "Recuperação bem sucedida!", + "decrypting": "A desencriptar…", "incorrectRecoveryKeyTitle": "Chave de recuperação incorreta", "incorrectRecoveryKeyBody": "A chave de recuperação inserida está incorreta", - "forgotPassword": "Esqueci a senha", - "enterYourRecoveryKey": "Insira sua chave de recuperação", - "noRecoveryKey": "Sem chave de recuperação?", + "forgotPassword": "Esqueceu-se da palavra-passe", + "enterYourRecoveryKey": "Insira a sua chave de recuperação", + "noRecoveryKey": "Não tem chave de recuperação?", "sorry": "Desculpe", - "noRecoveryKeyNoDecryption": "Devido à natureza do nosso protocolo de criptografia de ponta a ponta, seus dados não podem ser descriptografados sem sua senha ou chave de recuperação", - "verifyEmail": "Verificar e-mail", - "toResetVerifyEmail": "Para redefinir sua senha, verifique seu e-mail primeiramente.", - "checkInboxAndSpamFolder": "Verifique sua caixa de entrada (e spam) para concluir a verificação", + "noRecoveryKeyNoDecryption": "Devido à natureza do nosso protocolo de criptografia de ponta a ponta, os seus dados não podem ser descriptografados sem a sua palavra-passe ou a sua chave de recuperação", + "verifyEmail": "Verificar email", + "toResetVerifyEmail": "Para redefinir a sua palavra-passe, verifique primeiro o seu e-mail.", + "checkInboxAndSpamFolder": "Verifique a sua caixa de entrada (e spam) para concluir a verificação", "tapToEnterCode": "Toque para inserir código", "resendEmail": "Reenviar e-mail", - "weHaveSendEmailTo": "Enviamos um e-mail à {email}", + "weHaveSendEmailTo": "Enviamos um e-mail para {email}", "@weHaveSendEmailTo": { "description": "Text to indicate that we have sent a mail to the user", "placeholders": { @@ -66,17 +64,17 @@ } } }, - "setPasswordTitle": "Definir senha", - "changePasswordTitle": "Alterar senha", - "resetPasswordTitle": "Redefinir senha", - "encryptionKeys": "Chaves de criptografia", - "passwordWarning": "Nós não armazenamos esta senha, se você esquecer, nós não poderemos descriptografar seus dados", - "enterPasswordToEncrypt": "Insira uma senha que podemos usar para criptografar seus dados", - "enterNewPasswordToEncrypt": "Insira uma senha nova para criptografar seus dados", + "setPasswordTitle": "Definir palavra-passe", + "changePasswordTitle": "Alterar palavra-passe", + "resetPasswordTitle": "Redefinir palavra-passe", + "encryptionKeys": "Chaves de encriptação", + "passwordWarning": "Não armazenamos esta palavra-passe, se você a esquecer, não podemos desencriptar os seus dados", + "enterPasswordToEncrypt": "Inserir uma palavra-passe para encriptar os seus dados", + "enterNewPasswordToEncrypt": "Inserir uma nova palavra-passe para encriptar os seus dados", "weakStrength": "Fraca", "strongStrength": "Forte", - "moderateStrength": "Moderado", - "passwordStrength": "Força da senha: {passwordStrengthValue}", + "moderateStrength": "Moderada", + "passwordStrength": "Força da palavra-passe: {passwordStrengthValue}", "@passwordStrength": { "description": "Text to indicate the password strength", "placeholders": { @@ -88,39 +86,39 @@ }, "message": "Password Strength: {passwordStrengthText}" }, - "passwordChangedSuccessfully": "Senha alterada com sucesso", - "generatingEncryptionKeys": "Gerando chaves de criptografia...", - "pleaseWait": "Aguarde...", + "passwordChangedSuccessfully": "Palavra-passe alterada com sucesso", + "generatingEncryptionKeys": "Gerando chaves de encriptação...", + "pleaseWait": "Por favor, aguarde ...", "continueLabel": "Continuar", "insecureDevice": "Dispositivo inseguro", - "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Desculpe, não foi possível gerar chaves seguras neste dispositivo.\n\ninicie sessão com um dispositivo diferente.", + "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Desculpe, não foi possível gerar chaves seguras neste dispositivo.\n\npor favor iniciar sessão com um dispositivo diferente.", "howItWorks": "Como funciona", - "encryption": "Criptografia", - "ackPasswordLostWarning": "Eu entendo que se eu perder minha senha, posso perder meus dados, já que meus dados são criptografados de ponta a ponta.", - "privacyPolicyTitle": "Política de Privacidade", + "encryption": "Encriptação", + "ackPasswordLostWarning": "Eu entendo que se eu perder a minha palavra-passe, posso perder os meus dados já que esses dados são encriptados de ponta a ponta.", + "privacyPolicyTitle": "Política de privacidade", "termsOfServicesTitle": "Termos", - "signUpTerms": "Eu concordo com os termos de serviço e a política de privacidade", - "logInLabel": "Entrar", - "loginTerms": "Ao clicar em entrar, eu concordo com os termos de serviço e a política de privacidade", + "signUpTerms": "Eu concordo com os termos de serviço e política de privacidade", + "logInLabel": "Iniciar sessão", + "loginTerms": "Ao clicar em iniciar sessão, eu concordo com os termos de serviço e política de privacidade", "changeEmail": "Alterar e-mail", - "enterYourPassword": "Insira sua senha", + "enterYourPassword": "Introduza a sua palavra-passe", "welcomeBack": "Bem-vindo(a) de volta!", - "contactSupport": "Contatar suporte", - "incorrectPasswordTitle": "Senha incorreta", - "pleaseTryAgain": "Tente novamente", - "recreatePasswordTitle": "Redefinir senha", + "contactSupport": "Contactar o suporte", + "incorrectPasswordTitle": "Palavra-passe incorreta", + "pleaseTryAgain": "Por favor, tente novamente", + "recreatePasswordTitle": "Recriar palavra-passe", "useRecoveryKey": "Usar chave de recuperação", - "recreatePasswordBody": "O dispositivo atual não é poderoso o suficiente para verificar sua senha, no entanto, nós podemos regenerar numa maneira que funciona em todos os dispositivos.\n\nEntre usando a chave de recuperação e regenere sua senha (você pode usar a mesma novamente se desejar).", - "verifyPassword": "Verificar senha", + "recreatePasswordBody": "O dispositivo atual não é suficientemente poderoso para verificar a palavra-passe, mas podemos regenerar novamente de uma maneira que funcione no seu dispositivo.\n\nPor favor, iniciar sessão utilizando código de recuperação e gerar novamente a sua palavra-passe (pode utilizar a mesma se quiser).", + "verifyPassword": "Verificar palavra-passe", "recoveryKey": "Chave de recuperação", - "recoveryKeyOnForgotPassword": "Caso você esqueça sua senha, a única maneira de recuperar seus dados é com esta chave.", - "recoveryKeySaveDescription": "Não armazenamos esta chave, salve esta chave de 24 palavras em um lugar seguro.", - "doThisLater": "Fazer isso depois", - "saveKey": "Salvar chave", + "recoveryKeyOnForgotPassword": "Se esquecer sua palavra-passe, a única maneira de recuperar os seus dados é com esta chave.", + "recoveryKeySaveDescription": "Não armazenamos essa chave, por favor, guarde esta chave de 24 palavras num lugar seguro.", + "doThisLater": "Fazer isto mais tarde", + "saveKey": "Guardar chave", "recoveryKeyCopiedToClipboard": "Chave de recuperação copiada para a área de transferência", "recoverAccount": "Recuperar conta", "recover": "Recuperar", - "dropSupportEmail": "Envie um e-mail para {supportEmail} a partir do seu endereço de e-mail registrado", + "dropSupportEmail": "Envie um e-mail para {supportEmail} a partir do seu endereço de e-mail registado", "@dropSupportEmail": { "placeholders": { "supportEmail": { @@ -132,39 +130,39 @@ }, "twofactorSetup": "Configuração de dois fatores", "enterCode": "Insira o código", - "scanCode": "Escanear código", - "codeCopiedToClipboard": "Código copiado para a área de transferência", - "copypasteThisCodentoYourAuthenticatorApp": "Copie e cole este código\npara o aplicativo autenticador", + "scanCode": "Ler código Qr", + "codeCopiedToClipboard": "Código copiado para área de transferência", + "copypasteThisCodentoYourAuthenticatorApp": "Copie e cole este código\nno seu aplicativo de autenticação", "tapToCopy": "toque para copiar", - "scanThisBarcodeWithnyourAuthenticatorApp": "Escaneie este código de barras com\no aplicativo autenticador", - "enterThe6digitCodeFromnyourAuthenticatorApp": "Digite o código de 6 dígitos do\naplicativo de autenticador", + "scanThisBarcodeWithnyourAuthenticatorApp": "Leia este código com a sua aplicação dois fatores.", + "enterThe6digitCodeFromnyourAuthenticatorApp": "Introduzir o código de 6 dígitos da\nsua aplicação de autenticação", "confirm": "Confirmar", "setupComplete": "Configuração concluída", - "saveYourRecoveryKeyIfYouHaventAlready": "Salve sua chave de recuperação, se você ainda não fez", - "thisCanBeUsedToRecoverYourAccountIfYou": "Isso pode ser usado para recuperar sua conta se você perder seu segundo fator", + "saveYourRecoveryKeyIfYouHaventAlready": "Guarde a sua chave de recuperação, caso ainda não o tenha feito", + "thisCanBeUsedToRecoverYourAccountIfYou": "Isto pode ser usado para recuperar sua conta se você perder seu segundo fator", "twofactorAuthenticationPageTitle": "Autenticação de dois fatores", - "lostDevice": "Perdeu o dispositivo?", + "lostDevice": "Perdeu o seu dispositívo?", "verifyingRecoveryKey": "Verificando chave de recuperação...", "recoveryKeyVerified": "Chave de recuperação verificada", - "recoveryKeySuccessBody": "Ótimo! Sua chave de recuperação é válida. Obrigada por verificar.\n\nLembre-se de manter sua chave de recuperação copiada com segurança.", - "invalidRecoveryKey": "A chave de recuperação que você inseriu não é válida. Certifique-se de conter 24 caracteres, e verifique a ortografia de cada um deles.\n\nSe você inseriu um código de recuperação mais antigo, verifique se ele tem 64 caracteres e verifique cada um deles.", + "recoveryKeySuccessBody": "Ótimo! A sua chave de recuperação é válida. Obrigado por verificar.\n\nLembre-se de manter cópia de segurança da sua chave de recuperação.", + "invalidRecoveryKey": "A chave de recuperação que inseriu não é válida. Por favor, certifique-se que ela contém 24 palavras e verifique a ortografia de cada uma.\n\nSe inseriu um código de recuperação mais antigo, certifique-se de que tem 64 caracteres e verifique cada um deles.", "invalidKey": "Chave inválida", "tryAgain": "Tente novamente", "viewRecoveryKey": "Ver chave de recuperação", "confirmRecoveryKey": "Confirmar chave de recuperação", - "recoveryKeyVerifyReason": "Sua chave de recuperação é a única maneira de recuperar suas fotos se você esqueceu sua senha. Você pode encontrar sua chave de recuperação em Opções > Conta.\n\nInsira sua chave de recuperação aqui para verificar se você a salvou corretamente.", - "confirmYourRecoveryKey": "Confirme sua chave de recuperação", + "recoveryKeyVerifyReason": "A sua chave de recuperação é a única forma de recuperar as suas fotografias se se esquecer da sua palavra-passe. Pode encontrar a sua chave de recuperação em Definições > Conta.\n\n\nIntroduza aqui a sua chave de recuperação para verificar se a guardou corretamente.", + "confirmYourRecoveryKey": "Confirmar chave de recuperação", "addViewer": "Adicionar visualizador", "addCollaborator": "Adicionar colaborador", "addANewEmail": "Adicionar um novo e-mail", - "orPickAnExistingOne": "Ou escolha um existente", + "orPickAnExistingOne": "Ou escolha um já existente", "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": "Os colaboradores podem adicionar fotos e vídeos ao álbum compartilhado.", - "enterEmail": "Inserir e-mail", - "albumOwner": "Proprietário", + "enterEmail": "Digite o e-mail", + "albumOwner": "Dono", "@albumOwner": { "description": "Role of the album owner" }, - "you": "Você", + "you": "Tu", "collaborator": "Colaborador", "addMore": "Adicionar mais", "@addMore": { @@ -176,37 +174,35 @@ "@removeParticipant": { "description": "menuSectionTitle for removing a participant" }, - "manage": "Gerenciar", + "manage": "Gerir", "addedAs": "Adicionado como", - "changePermissions": "Alterar permissões?", + "changePermissions": "Alterar permissões", "yesConvertToViewer": "Sim, converter para visualizador", - "cannotAddMorePhotosAfterBecomingViewer": "{user} Não poderá adicionar mais fotos a este álbum\n\nEles ainda conseguirão remover fotos existentes adicionadas por eles", + "cannotAddMorePhotosAfterBecomingViewer": "{user} não será capaz de adicionar mais fotos a este álbum\n\nEles ainda serão capazes de remover fotos existentes adicionadas por eles", "allowAddingPhotos": "Permitir adicionar fotos", "@allowAddingPhotos": { "description": "Switch button to enable uploading photos to a public link" }, - "allowAddPhotosDescription": "Permitir que as pessoas com link também adicionem fotos ao álbum compartilhado.", - "passwordLock": "Bloqueio por senha", - "canNotOpenTitle": "Não pôde abrir este álbum", - "canNotOpenBody": "Desculpe, este álbum não pode ser aberto no aplicativo.", - "disableDownloadWarningTitle": "Por favor, saiba que", - "disableDownloadWarningBody": "Os visualizadores podem fazer capturas de tela ou salvar uma cópia de suas fotos usando ferramentas externas", + "allowAddPhotosDescription": "Permitir que pessoas com o link também adicionem fotos ao álbum compartilhado.", + "passwordLock": "Bloqueio da palavra-passe", + "disableDownloadWarningTitle": "Por favor, observe", + "disableDownloadWarningBody": "Visualizadores ainda podem fazer capturas de tela ou salvar uma cópia das suas fotos usando ferramentas externas", "allowDownloads": "Permitir downloads", - "linkDeviceLimit": "Limite do dispositivo", + "linkDeviceLimit": "Limite de dispositivo", "noDeviceLimit": "Nenhum", "@noDeviceLimit": { "description": "Text to indicate that there is limit on number of devices" }, - "linkExpiry": "Expiração do link", + "linkExpiry": "Link expirado", "linkExpired": "Expirado", "linkEnabled": "Ativado", "linkNeverExpires": "Nunca", - "expiredLinkInfo": "O link expirou. Selecione um novo tempo de expiração ou desative a expiração do link.", - "setAPassword": "Definir senha", + "expiredLinkInfo": "Este link expirou. Por favor, selecione um novo tempo de expiração ou desabilite a expiração do link.", + "setAPassword": "Definir uma palavra-passe", "lockButtonLabel": "Bloquear", - "enterPassword": "Inserir senha", + "enterPassword": "Introduzir palavra-passe", "removeLink": "Remover link", - "manageLink": "Gerenciar link", + "manageLink": "Gerir link", "linkExpiresOn": "O link expirará em {expiryTime}", "albumUpdated": "Álbum atualizado", "never": "Nunca", @@ -214,12 +210,12 @@ "@custom": { "description": "Label for setting custom value for link expiry" }, - "after1Hour": "Após 1 hora", - "after1Day": "Após 1 dia", - "after1Week": "Após 1 semana", - "after1Month": "Após 1 mês", - "after1Year": "Após 1 ano", - "manageParticipants": "Gerenciar", + "after1Hour": "Depois de 1 Hora", + "after1Day": "Depois de 1 dia", + "after1Week": "Depois de 1 semana", + "after1Month": "Depois de 1 mês", + "after1Year": "Depois de 1 ano", + "manageParticipants": "Gerir", "albumParticipantsCount": "{count, plural, =0 {Nenhum participante} =1 {1 participante} other {{count} participantes}}", "@albumParticipantsCount": { "placeholders": { @@ -230,17 +226,17 @@ }, "description": "Number of participants in an album, including the album owner." }, - "collabLinkSectionDescription": "Crie um link para permitir que as pessoas adicionem e vejam fotos no seu álbum compartilhado sem a necessidade do aplicativo ou uma conta Ente. Ótimo para colecionar fotos de eventos.", + "collabLinkSectionDescription": "Criar um link para permitir que as pessoas adicionem e visualizem fotos em seu álbum compartilhado sem precisar de um aplicativo Ente ou conta. Ótimo para coletar fotos do evento.", "collectPhotos": "Coletar fotos", "collaborativeLink": "Link colaborativo", - "shareWithNonenteUsers": "Compartilhar com usuários não ente", + "shareWithNonenteUsers": "Compartilhar com usuários que não usam Ente", "createPublicLink": "Criar link público", "sendLink": "Enviar link", "copyLink": "Copiar link", "linkHasExpired": "O link expirou", - "publicLinkEnabled": "Link público ativo", - "shareALink": "Compartilhar link", - "sharedAlbumSectionDescription": "Criar álbuns compartilhados e colaborativos com outros usuários Ente, incluindo usuários em planos gratuitos.", + "publicLinkEnabled": "Link público ativado", + "shareALink": "Partilhar um link", + "sharedAlbumSectionDescription": "Criar álbuns compartilhados e colaborativos com outros usuários da Ente, incluindo usuários em planos gratuitos.", "shareWithPeopleSectionTitle": "{numberOfPeople, plural, =0 {Compartilhe com pessoas específicas} =1 {Compartilhado com 1 pessoa} other {Compartilhado com {numberOfPeople} pessoas}}", "@shareWithPeopleSectionTitle": { "placeholders": { @@ -251,8 +247,8 @@ } }, "thisIsYourVerificationId": "Este é o seu ID de verificação", - "someoneSharingAlbumsWithYouShouldSeeTheSameId": "Alguém compartilhando álbuns com você deve ver o mesmo ID no dispositivo.", - "howToViewShareeVerificationID": "Peça-os para manterem pressionado no endereço de e-mail na tela de opções, e verifique-se os IDs de ambos os dispositivos correspondem.", + "someoneSharingAlbumsWithYouShouldSeeTheSameId": "Alguém compartilhando álbuns com você deve ver o mesmo ID no seu dispositivo.", + "howToViewShareeVerificationID": "Por favor, peça-lhes para pressionar longamente o endereço de e-mail na tela de configurações e verifique se os IDs de ambos os dispositivos coincidem.", "thisIsPersonVerificationId": "Este é o ID de verificação de {email}", "@thisIsPersonVerificationId": { "placeholders": { @@ -262,44 +258,44 @@ } } }, - "verificationId": "ID de verificação", - "verifyEmailID": "Verificar {email}", - "emailNoEnteAccount": "{email} não tem uma conta Ente.\n\nEnvie-os um convite para compartilhar fotos.", - "shareMyVerificationID": "Aqui está meu ID de verificação para o ente.io: {verificationID}", - "shareTextConfirmOthersVerificationID": "Ei, você pode confirmar se este ID de verificação do ente.io é seu?: {verificationID}", - "somethingWentWrong": "Algo deu errado", + "verificationId": "ID de Verificação", + "verifyEmailID": "Verificar e-mail", + "emailNoEnteAccount": "{email} não possui uma conta Ente.\n\nEnvie um convite para compartilhar fotos.", + "shareMyVerificationID": "Aqui está o meu ID de verificação: {verificationID} para ente.io.", + "shareTextConfirmOthersVerificationID": "Ei, você pode confirmar que este é seu ID de verificação do ente.io: {verificationID}", + "somethingWentWrong": "Ocorreu um erro", "sendInvite": "Enviar convite", - "shareTextRecommendUsingEnte": "Baixe o Ente para que nós possamos compartilhar com facilidade fotos e vídeos de qualidade original\n\nhttps://ente.io", + "shareTextRecommendUsingEnte": "Descarregue o Ente para poder partilhar facilmente fotografias e vídeos de qualidade original\n\n\nhttps://ente.io", "done": "Concluído", "applyCodeTitle": "Aplicar código", - "enterCodeDescription": "Insira o código fornecido pelo seu amigo para reivindicar o armazenamento grátis para os dois", + "enterCodeDescription": "Introduza o código fornecido pelo seu amigo para obter armazenamento gratuito para ambos", "apply": "Aplicar", - "failedToApplyCode": "Falhou ao aplicar código", - "enterReferralCode": "Inserir código de referência", + "failedToApplyCode": "Falha ao aplicar código", + "enterReferralCode": "Insira o código de referência", "codeAppliedPageTitle": "Código aplicado", - "changeYourReferralCode": "Alterar código de referência", + "changeYourReferralCode": "Alterar o código de referência", "change": "Alterar", - "unavailableReferralCode": "Desculpe, este código está indisponível.", - "codeChangeLimitReached": "Desculpe, você atingiu o limite de mudanças de código.", + "unavailableReferralCode": "Desculpe, este código não está disponível.", + "codeChangeLimitReached": "Desculpe, você atingiu o limite de alterações de código.", "onlyFamilyAdminCanChangeCode": "Entre em contato com {familyAdminEmail} para alterar o seu código.", "storageInGB": "{storageAmountInGB} GB", - "claimed": "Reivindicado", + "claimed": "Reclamado", "@claimed": { "description": "Used to indicate storage claimed, like 10GB Claimed" }, "details": "Detalhes", - "claimMore": "Reivindique mais!", + "claimMore": "Reclamar mais!", "theyAlsoGetXGb": "Eles também recebem {storageAmountInGB} GB", - "freeStorageOnReferralSuccess": "{storageAmountInGB} GB cada vez que alguém se inscrever a um plano pago e aplicar seu código", - "shareTextReferralCode": "Código de referência do Ente: {referralCode} \n\nAplique-o em Configurações → Geral → Referências para obter {referralStorageInGB} GB grátis após a sua inscrição num plano pago\n\nhttps://ente.io", - "claimFreeStorage": "Reivindicar armazenamento grátis", - "inviteYourFriends": "Convide seus amigos", - "failedToFetchReferralDetails": "Não foi possível buscar os detalhes de referência. Tente novamente mais tarde.", + "freeStorageOnReferralSuccess": "{storageAmountInGB} GB sempre que alguém se inscreve num plano pago e aplica o seu código", + "shareTextReferralCode": "Insira o código de referência: {referralCode} \n\nAplique-o em Configurações → Geral → Indicações para obter {referralStorageInGB} GB gratuitamente após a sua inscrição para um plano pago\n\nhttps://ente.io", + "claimFreeStorage": "Solicitar armazenamento gratuito", + "inviteYourFriends": "Convide os seus amigos", + "failedToFetchReferralDetails": "Não foi possível obter detalhes de indicação. Por favor, tente novamente mais tarde.", "referralStep1": "1. Envie este código aos seus amigos", - "referralStep2": "2. Eles então se inscrevem num plano pago", - "referralStep3": "3. Ambos os dois ganham {storageInGB} GB* grátis", - "referralsAreCurrentlyPaused": "As referências estão atualmente pausadas", - "youCanAtMaxDoubleYourStorage": "* Você pode duplicar seu armazenamento ao máximo", + "referralStep2": "2. Eles se inscrevem em um plano pago", + "referralStep3": "3. Ambos ganham {storageInGB} GB* grátis", + "referralsAreCurrentlyPaused": "As referências estão atualmente em pausa", + "youCanAtMaxDoubleYourStorage": "* Você pode duplicar seu armazenamento no máximo", "claimedStorageSoFar": "{isFamilyMember, select,true {Sua família reinvidicou {storageAmountInGb} GB até então}false {Você reinvindicou {storageAmountInGb} GB até então}other {Você reinvindicou {storageAmountInGb} GB até então!}}", "@claimedStorageSoFar": { "placeholders": { @@ -313,17 +309,17 @@ } } }, - "faq": "Perguntas frequentes", + "faq": "Perguntas Frequentes", "help": "Ajuda", "oopsSomethingWentWrong": "Ops, algo deu errado", - "peopleUsingYourCode": "Pessoas que usam seu código", + "peopleUsingYourCode": "Pessoas que utilizam seu código", "eligible": "elegível", "total": "total", "codeUsedByYou": "Código usado por você", - "freeStorageClaimed": "Armazenamento grátis reivindicado", - "freeStorageUsable": "Armazenamento disponível", + "freeStorageClaimed": "Armazenamento gratuito reclamado", + "freeStorageUsable": "Armazenamento livre utilizável", "usableReferralStorageInfo": "O armazenamento disponível é limitado pelo seu plano atual. O excesso de armazenamento reivindicado tornará automaticamente útil quando você atualizar seu plano.", - "removeFromAlbumTitle": "Remover do álbum?", + "removeFromAlbumTitle": "Remover do álbum", "removeFromAlbum": "Remover do álbum", "itemsWillBeRemovedFromAlbum": "Os itens selecionados serão removidos deste álbum", "removeShareItemsWarning": "Alguns dos itens que você está removendo foram adicionados por outras pessoas, e você perderá o acesso a eles", @@ -331,61 +327,45 @@ "removingFromFavorites": "Removendo dos favoritos...", "sorryCouldNotAddToFavorites": "Desculpe, não foi possível adicionar aos favoritos!", "sorryCouldNotRemoveFromFavorites": "Desculpe, não foi possível remover dos favoritos!", - "subscribeToEnableSharing": "Você precisa de uma inscrição paga ativa para ativar o compartilhamento.", - "subscribe": "Inscrever-se", - "canOnlyRemoveFilesOwnedByYou": "Só pode remover arquivos de sua propriedade", + "subscribeToEnableSharing": "Você precisa de uma assinatura paga ativa para ativar o compartilhamento.", + "subscribe": "Subscrever", + "canOnlyRemoveFilesOwnedByYou": "", "deleteSharedAlbum": "Excluir álbum compartilhado?", - "deleteAlbum": "Excluir álbum", - "deleteAlbumDialog": "Também excluir as fotos (e vídeos) presentes neste álbum de todos os outros álbuns que eles fazem parte?", - "deleteSharedAlbumDialogBody": "O álbum será apagado para todos\n\nVocê perderá o acesso a fotos compartilhadas neste álbum que pertencem aos outros", - "yesRemove": "Sim, excluir", - "creatingLink": "Criando link...", + "deleteAlbum": "Apagar álbum", + "deleteAlbumDialog": "Eliminar também as fotos (e vídeos) presentes neste álbum de all os outros álbuns de que fazem parte?", + "deleteSharedAlbumDialogBody": "O álbum será apagado para todos\n\nVocê perderá o acesso a fotos compartilhadas neste álbum que são propriedade de outros", + "yesRemove": "Sim, remover", + "creatingLink": "Criar link...", "removeWithQuestionMark": "Remover?", - "removeParticipantBody": "{userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por eles também serão removidas do álbum", + "removeParticipantBody": "{userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por elas também serão removidas do álbum", "keepPhotos": "Manter fotos", - "deletePhotos": "Excluir fotos", - "inviteToEnte": "Convidar ao Ente", + "deletePhotos": "Apagar fotos", + "inviteToEnte": "Convidar para Ente", "removePublicLink": "Remover link público", - "disableLinkMessage": "Isso removerá o link público para acessar \"{albumName}\".", - "sharing": "Compartilhando...", - "youCannotShareWithYourself": "Você não pode compartilhar consigo mesmo", - "archive": "Arquivo", - "createAlbumActionHint": "Pressione para selecionar fotos e clique em + para criar um álbum", - "importing": "Importando....", - "failedToLoadAlbums": "Falhou ao carregar álbuns", + "disableLinkMessage": "Isto removerá o link público para acessar \"{albumName}\".", + "sharing": "Partilhar...", + "youCannotShareWithYourself": "Não podes partilhar contigo mesmo", + "archive": "............", + "createAlbumActionHint": "Pressione e segure para selecionar fotos e clique em + para criar um álbum", + "importing": "A importar...", + "failedToLoadAlbums": "Falha ao carregar álbuns", "hidden": "Oculto", - "authToViewYourHiddenFiles": "Autentique-se para visualizar seus arquivos ocultos", - "authToViewTrashedFiles": "Autentique-se para ver seus arquivos excluídos", - "trash": "Lixeira", + "authToViewYourHiddenFiles": "Por favor, autentique para ver seus arquivos ocultos", + "trash": "Lixo", "uncategorized": "Sem categoria", "videoSmallCase": "vídeo", "photoSmallCase": "foto", - "singleFileDeleteHighlight": "Ele será excluído de todos os álbuns.", - "singleFileInBothLocalAndRemote": "Este {fileType} está no Ente e em seu dispositivo.", - "singleFileInRemoteOnly": "Este {fileType} será excluído do Ente.", - "singleFileDeleteFromDevice": "Este {fileType} será excluído do dispositivo.", - "deleteFromEnte": "Excluir do Ente", - "yesDelete": "Sim, excluir", - "movedToTrash": "Movido para a lixeira", - "deleteFromDevice": "Excluir do dispositivo", - "deleteFromBoth": "Excluir de ambos", + "singleFileDeleteHighlight": "Será eliminado de todos os álbuns.", + "singleFileInBothLocalAndRemote": "Este {fileType} encontra-se tanto no Ente como no seu dispositivo.", + "singleFileInRemoteOnly": "Este {fileType} será eliminado do Ente.", + "singleFileDeleteFromDevice": "Este {fileType} será eliminado do seu dispositivo.", + "deleteFromEnte": "Apagar do Ente", + "yesDelete": "Sim, apagar", + "movedToTrash": "Mover para o lixo", + "deleteFromDevice": "Apagar do dispositivo", + "deleteFromBoth": "Apagar de ambos", "newAlbum": "Novo álbum", "albums": "Álbuns", - "memoryCount": "{count, plural, zero{sem memórias}\none{{formattedCount} memória}\nother{{formattedCount} memórias}}", - "@memoryCount": { - "description": "The text to display the number of memories", - "type": "text", - "placeholders": { - "count": { - "example": "1", - "type": "int" - }, - "formattedCount": { - "type": "String", - "example": "11.513, 11,511" - } - } - }, "selectedPhotos": "{count} selecionado(s)", "@selectedPhotos": { "description": "Display the number of selected photos", @@ -412,31 +392,30 @@ } } }, - "advancedSettings": "Avançado", + "advancedSettings": "Definições avançadas", "@advancedSettings": { "description": "The text to display in the advanced settings section" }, - "photoGridSize": "Tamanho da grade de fotos", - "manageDeviceStorage": "Gerenciar cache do dispositivo", + "photoGridSize": "Tamanho da grelha de fotos", "manageDeviceStorageDesc": "Reveja e limpe o armazenamento de cache local.", "machineLearning": "Aprendizagem automática", "mlConsent": "Ativar aprendizagem automática", "mlConsentTitle": "Ativar aprendizagem automática?", - "mlConsentDescription": "Se você ativar a aprendizagem automática, o Ente irá extrair informações como geometria de rosto dos arquivos, incluindo os compartilhados com você.\n\nIsso acontecerá no seu dispositivo, qualquer informação biométrica gerada será criptografada ponta a ponta.", - "mlConsentPrivacy": "Clique aqui para mais detalhes sobre este recurso na política de privacidade", + "mlConsentDescription": "Se ativar a aprendizagem automática, o Ente extrairá informações como a geometria do rosto de ficheiros, incluindo os partilhados consigo.\n\n\nIsto acontecerá no seu dispositivo e todas as informações biométricas geradas serão encriptadas de ponta a ponta.", + "mlConsentPrivacy": "Por favor, clique aqui para mais detalhes sobre este recurso na nossa política de privacidade", "mlConsentConfirmation": "Eu entendo, e desejo ativar a aprendizagem automática", - "magicSearch": "Busca mágica", - "discover": "Explorar", + "magicSearch": "Pesquisa mágica", + "discover": "Descobrir", "@discover": { "description": "The text to display for the discover section under which we show receipts, screenshots, sunsets, greenery, etc." }, "discover_identity": "Identidade", - "discover_screenshots": "Capturas de tela", + "discover_screenshots": "Capturas de ecrã", "discover_receipts": "Recibos", "discover_notes": "Notas", "discover_memes": "Memes", "discover_visiting_cards": "Cartões de visita", - "discover_babies": "Bebês", + "discover_babies": "Bebés", "discover_pets": "Animais de estimação", "discover_selfies": "Selfies", "discover_wallpapers": "Papéis de parede", @@ -445,21 +424,21 @@ "discover_sunset": "Pôr do sol", "discover_hills": "Colinas", "discover_greenery": "Vegetação", - "mlIndexingDescription": "Note que a aprendizagem automática resultará em uso de bateria e largura de banda maior até que todos os itens forem indexados. Considere-se usar o aplicativo para notebook para uma indexação mais rápida, todos os resultados serão sincronizados automaticamente.", - "loadingModel": "Baixando modelos...", + "mlIndexingDescription": "Tenha em atenção que a aprendizagem automática resultará numa maior utilização da largura de banda e da bateria até que todos os itens sejam indexados. Considere utilizar a aplicação de ambiente de trabalho para uma indexação mais rápida, todos os resultados serão sincronizados automaticamente.", + "loadingModel": "Transferindo modelos...", "waitingForWifi": "Aguardando Wi-Fi...", - "status": "Estado", + "status": "Status", "indexedItems": "Itens indexados", "pendingItems": "Itens pendentes", "clearIndexes": "Limpar índices", - "selectFoldersForBackup": "Selecionar pastas para copiar com segurança", - "selectedFoldersWillBeEncryptedAndBackedUp": "As pastas selecionadas serão criptografadas e armazenadas em copiadas com segurança", + "selectFoldersForBackup": "Selecionar pastas para cópia de segurança", + "selectedFoldersWillBeEncryptedAndBackedUp": "As pastas selecionadas serão encriptadas e guardadas como cópia de segurança", "unselectAll": "Desmarcar tudo", "selectAll": "Selecionar tudo", "skip": "Pular", "updatingFolderSelection": "Atualizando seleção de pasta...", "itemCount": "{count, plural, one{{count} item} other{{count} itens}}", - "deleteItemCount": "{count, plural, =1 {Excluir {count} item} other {Excluir {count} itens}}", + "deleteItemCount": "{count, plural, =1 {Apagar {count} item} other {Apagar {count} itens}}", "duplicateItemsGroup": "{count} arquivos, {formattedSize} cada", "@duplicateItemsGroup": { "description": "Display the number of duplicate files and their size", @@ -477,58 +456,57 @@ }, "showMemories": "Mostrar memórias", "yearsAgo": "{count, plural, one{{count} ano atrás} other{{count} anos atrás}}", - "backupSettings": "Opções de cópia de segurança", + "backupSettings": "Definições da cópia de segurança", "backupStatus": "Status da cópia de segurança", "backupStatusDescription": "Os itens que foram salvos com segurança aparecerão aqui", - "backupOverMobileData": "Salvamento com segurança usando dados móveis", + "backupOverMobileData": "Cópia de segurança através dos dados móveis", "backupVideos": "Cópia de segurança de vídeos", "disableAutoLock": "Desativar bloqueio automático", - "deviceLockExplanation": "Desativa o bloqueio de tela quando o Ente está de fundo e têm uma cópia de segurança sendo feita. Isso normalmente não é necessário, no entanto, ajuda a envios grandes e importações iniciais de bibliotecas maiores concluírem mais rápido.", + "deviceLockExplanation": "Desativar o bloqueio do ecrã do dispositivo quando o Ente estiver em primeiro plano e houver uma cópia de segurança em curso. Normalmente, isto não é necessário, mas pode ajudar a que os grandes carregamentos e as importações iniciais de grandes bibliotecas sejam concluídos mais rapidamente.", "about": "Sobre", "weAreOpenSource": "Nós somos de código aberto!", "privacy": "Privacidade", "terms": "Termos", - "checkForUpdates": "Buscar atualizações", - "checkStatus": "Verificar estado", - "checking": "Verificando...", - "youAreOnTheLatestVersion": "Você está na versão mais recente", + "checkForUpdates": "Procurar atualizações", + "checkStatus": "Verificar status", + "checking": "A verificar...", + "youAreOnTheLatestVersion": "Está a utilizar a versão mais recente", "account": "Conta", - "manageSubscription": "Gerenciar assinatura", + "manageSubscription": "Gerir subscrição", "authToChangeYourEmail": "Por favor, autentique-se para alterar o seu e-mail", - "changePassword": "Alterar senha", - "authToChangeYourPassword": "Autentique para alterar sua senha", + "changePassword": "Alterar palavra-passe", + "authToChangeYourPassword": "Por favor, autentique-se para alterar a palavra-passe", "emailVerificationToggle": "Verificação por e-mail", - "authToChangeEmailVerificationSetting": "Autentique-se para alterar o e-mail de verificação", - "exportYourData": "Exportar dados", - "logout": "Encerrar sessão", - "authToInitiateAccountDeletion": "Autentique para iniciar a exclusão de conta", - "areYouSureYouWantToLogout": "Você tem certeza que quer encerrar sessão?", - "yesLogout": "Sim, encerrar sessão", - "aNewVersionOfEnteIsAvailable": "Uma nova versão do Ente está disponível.", + "authToChangeEmailVerificationSetting": "Por favor, autentique-se para alterar a verificação de e-mail", + "exportYourData": "Exportar os seus dados", + "logout": "Terminar sessão", + "authToInitiateAccountDeletion": "Autentique-se para iniciar a eliminação da conta", + "areYouSureYouWantToLogout": "Tem certeza que deseja terminar a sessão?", + "yesLogout": "Sim, terminar sessão", + "aNewVersionOfEnteIsAvailable": "Está disponível uma nova versão do Ente.", "update": "Atualizar", "installManually": "Instalar manualmente", "criticalUpdateAvailable": "Atualização crítica disponível", "updateAvailable": "Atualização disponível", "ignoreUpdate": "Ignorar", - "downloading": "Baixando...", - "cannotDeleteSharedFiles": "Não é possível excluir arquivos compartilhados", - "theDownloadCouldNotBeCompleted": "A instalação não pôde ser concluída", + "downloading": "A transferir...", + "cannotDeleteSharedFiles": "Não é possível eliminar ficheiros partilhados", + "theDownloadCouldNotBeCompleted": "Não foi possível concluir o download.", "retry": "Tentar novamente", - "backedUpFolders": "Pastas copiadas com segurança", + "backedUpFolders": "Pastas com cópia de segurança", "backup": "Cópia de segurança", - "freeUpDeviceSpace": "Liberar espaço no dispositivo", - "freeUpDeviceSpaceDesc": "Economize espaço em seu dispositivo por limpar arquivos já salvos com segurança.", + "freeUpDeviceSpace": "Libertar espaço no dispositivo", + "freeUpDeviceSpaceDesc": "Poupe espaço no seu dispositivo limpando ficheiros dos quais já foi feita uma cópia de segurança.", "allClear": "✨ Tudo limpo", - "noDeviceThatCanBeDeleted": "Você não tem arquivos neste dispositivo que possam ser excluídos", - "removeDuplicates": "Excluir duplicatas", - "removeDuplicatesDesc": "Revise e remova arquivos que são duplicatas exatas.", - "viewLargeFiles": "Arquivos grandes", - "viewLargeFilesDesc": "Ver arquivos que consumem a maior parte do armazenamento.", - "noDuplicates": "✨ Sem duplicatas", - "youveNoDuplicateFilesThatCanBeCleared": "Você não possui nenhum arquivo duplicado que possa ser excluído", + "noDeviceThatCanBeDeleted": "Você não tem arquivos neste dispositivo que possam ser apagados", + "removeDuplicates": "Remover duplicados", + "removeDuplicatesDesc": "Rever e remover ficheiros que sejam duplicados exatos.", + "viewLargeFiles": "Ficheiros grandes", + "viewLargeFilesDesc": "Ver os ficheiros que estão a consumir a maior quantidade de armazenamento.", + "noDuplicates": "✨ Sem duplicados", "success": "Sucesso", - "rateUs": "Avaliar", - "remindToEmptyDeviceTrash": "Também vazio \"Excluído recentemente\" de \"Opções\" -> \"Armazenamento\" para reivindicar espaço liberado", + "rateUs": "Avalie-nos", + "remindToEmptyDeviceTrash": "Esvazie também a opção “Eliminados recentemente” em “Definições” -> “Armazenamento” para reclamar o espaço libertado", "youHaveSuccessfullyFreedUp": "Você liberou {storageSaved} com sucesso!", "@youHaveSuccessfullyFreedUp": { "description": "The text to display when the user has successfully freed up storage", @@ -540,9 +518,9 @@ } } }, - "remindToEmptyEnteTrash": "Também esvazie sua \"Lixeira\" para reivindicar o espaço liberado", + "remindToEmptyEnteTrash": "Esvazie também o seu “Lixo” para reivindicar o espaço libertado", "sparkleSuccess": "✨ Sucesso", - "duplicateFileCountWithStorageSaved": "Você limpou {count, plural, one{{count} arquivo duplicado} other{{count} arquivos duplicados}}, salvando ({storageSaved}!)", + "duplicateFileCountWithStorageSaved": "Você limpou {count, plural, one{{count} arquivo duplicado} other{{count} arquivos duplicados}}, guardando ({storageSaved}!)", "@duplicateFileCountWithStorageSaved": { "description": "The text to display when the user has successfully cleaned up duplicate files", "type": "text", @@ -560,43 +538,43 @@ "familyPlans": "Planos familiares", "referrals": "Referências", "notifications": "Notificações", - "sharedPhotoNotifications": "Novas fotos compartilhadas", - "sharedPhotoNotificationsExplanation": "Receber notificações quando alguém adicionar uma foto a um álbum compartilhado que você faz parte", + "sharedPhotoNotifications": "Novas fotos partilhadas", + "sharedPhotoNotificationsExplanation": "Receber notificações quando alguém adiciona uma foto a um álbum partilhado do qual faz parte", "advanced": "Avançado", "general": "Geral", "security": "Segurança", - "authToViewYourRecoveryKey": "Autentique para ver sua chave de recuperação", + "authToViewYourRecoveryKey": "Por favor, autentique-se para ver a chave de recuperação", "twofactor": "Dois fatores", - "authToConfigureTwofactorAuthentication": "Autentique para configurar a autenticação de dois fatores", - "lockscreen": "Tela de bloqueio", - "authToChangeLockscreenSetting": "Autentique para alterar a configuração da tela de bloqueio", + "authToConfigureTwofactorAuthentication": "Por favor, autentique para configurar a autenticação de dois fatores", + "lockscreen": "Ecrã de bloqueio", + "authToChangeLockscreenSetting": "Por favor, autentique-se para alterar a configuração da tela do ecrã de bloqueio", "viewActiveSessions": "Ver sessões ativas", - "authToViewYourActiveSessions": "Autentique para ver as sessões ativas", + "authToViewYourActiveSessions": "Por favor, autentique-se para ver as suas sessões ativas", "disableTwofactor": "Desativar autenticação de dois fatores", - "confirm2FADisable": "Você tem certeza que queira desativar a autenticação de dois fatores?", + "confirm2FADisable": "Tem a certeza de que pretende desativar a autenticação de dois fatores?", "no": "Não", "yes": "Sim", - "social": "Redes sociais", - "rateUsOnStore": "Avalie-nos no {storeName}", + "social": "Social", + "rateUsOnStore": "Avalie-nos em {storeName}", "blog": "Blog", "merchandise": "Produtos", - "twitter": "Twitter/X", + "twitter": "Twitter", "mastodon": "Mastodon", "matrix": "Matrix", "discord": "Discord", "reddit": "Reddit", - "yourStorageDetailsCouldNotBeFetched": "Seus detalhes de armazenamento não puderam ser obtidos", - "reportABug": "Informar um erro", - "reportBug": "Informar erro", - "suggestFeatures": "Sugerir recurso", + "yourStorageDetailsCouldNotBeFetched": "Não foi possível obter os seus dados de armazenamento", + "reportABug": "Reporte um bug", + "reportBug": "Reportar bug", + "suggestFeatures": "Sugerir recursos", "support": "Suporte", "theme": "Tema", "lightTheme": "Claro", "darkTheme": "Escuro", "systemTheme": "Sistema", - "freeTrial": "Avaliação grátis", - "selectYourPlan": "Selecione seu plano", - "enteSubscriptionPitch": "O Ente preserva suas memórias, então eles sempre estão disponíveis para você, mesmo se você perder o dispositivo.", + "freeTrial": "Teste grátis", + "selectYourPlan": "Selecione o seu plano", + "enteSubscriptionPitch": "O Ente preserva as suas memórias, para que estejam sempre disponíveis, mesmo que perca o seu dispositivo.", "enteSubscriptionShareWithFamily": "Sua família também pode ser adicionada ao seu plano.", "currentUsageIs": "O uso atual é ", "@currentUsageIs": { @@ -607,22 +585,22 @@ "type": "text" }, "faqs": "Perguntas frequentes", - "renewsOn": "Renovação de assinatura em {endDate}", - "freeTrialValidTill": "A avaliação grátis acaba em {endDate}", + "renewsOn": "A subscrição é renovada em {endDate}", + "freeTrialValidTill": "Teste gratuito válido até {endDate}", "validTill": "Válido até {endDate}", - "addOnValidTill": "Seu complemento {storageAmount} é válido até {endDate}", - "playStoreFreeTrialValidTill": "Avaliação grátis válida até {endDate}.\nVocê pode alterar para um plano pago depois.", - "subWillBeCancelledOn": "Sua assinatura será cancelada em {endDate}", - "subscription": "Assinatura", + "addOnValidTill": "Seu addon {storageAmount} é válido até o momento {endDate}", + "playStoreFreeTrialValidTill": "Teste gratuito válido até {endDate}.\nVocê pode escolher um plano pago depois.", + "subWillBeCancelledOn": "A sua subscrição será cancelada em {endDate}", + "subscription": "Subscrição", "paymentDetails": "Detalhes de pagamento", - "manageFamily": "Gerenciar família", - "contactToManageSubscription": "Entre em contato conosco em support@ente.io para gerenciar sua assinatura {provider}.", - "renewSubscription": "Renovar assinatura", - "cancelSubscription": "Cancelar assinatura", - "areYouSureYouWantToRenew": "Deseja renovar?", - "yesRenew": "Sim", - "areYouSureYouWantToCancel": "Deseja cancelar?", - "yesCancel": "Sim", + "manageFamily": "Gerir família", + "contactToManageSubscription": "Contacte-nos em support@ente.io para gerir a sua subscrição {provider}", + "renewSubscription": "Renovar subscrição", + "cancelSubscription": "Cancelar subscrição", + "areYouSureYouWantToRenew": "Tem a certeza de que pretende renovar?", + "yesRenew": "Sim, Renovar", + "areYouSureYouWantToCancel": "Tem a certeza de que quer cancelar?", + "yesCancel": "Sim, cancelar", "failedToRenew": "Falhou ao renovar", "failedToCancel": "Falhou ao cancelar", "twoMonthsFreeOnYearlyPlans": "2 meses grátis em planos anuais", @@ -636,10 +614,10 @@ "description": "The text to display for yearly plans", "type": "text" }, - "confirmPlanChange": "Confirmar mudança de plano", - "areYouSureYouWantToChangeYourPlan": "Deseja trocar de plano?", - "youCannotDowngradeToThisPlan": "Você não pode rebaixar para este plano", - "cancelOtherSubscription": "Primeiramente cancele sua assinatura existente do {paymentProvider}", + "confirmPlanChange": "Confirmar alteração de plano", + "areYouSureYouWantToChangeYourPlan": "Tem a certeza de que pretende alterar o seu plano?", + "youCannotDowngradeToThisPlan": "Não é possível fazer o downgrade para este plano", + "cancelOtherSubscription": "Por favor, cancele primeiro a sua subscrição existente de {paymentProvider}", "@cancelOtherSubscription": { "description": "The text to display when the user has an existing subscription from a different payment provider", "type": "text", @@ -650,24 +628,24 @@ } } }, - "optionalAsShortAsYouLike": "Opcional, tão curto como quiser...", + "optionalAsShortAsYouLike": "Opcional, o mais breve que quiser...", "send": "Enviar", - "askCancelReason": "Sua assinatura foi cancelada. Deseja compartilhar o motivo?", - "thankYouForSubscribing": "Obrigado por assinar!", - "yourPurchaseWasSuccessful": "Sua compra foi efetuada com sucesso", - "yourPlanWasSuccessfullyUpgraded": "Seu plano foi atualizado com sucesso", - "yourPlanWasSuccessfullyDowngraded": "Seu plano foi rebaixado com sucesso", - "yourSubscriptionWasUpdatedSuccessfully": "Sua assinatura foi atualizada com sucesso", + "askCancelReason": "A sua subscrição foi cancelada. Gostaria de partilhar o motivo?", + "thankYouForSubscribing": "Obrigado pela sua subscrição!", + "yourPurchaseWasSuccessful": "Sua compra foi realizada com sucesso", + "yourPlanWasSuccessfullyUpgraded": "O seu plano foi atualizado com sucesso", + "yourPlanWasSuccessfullyDowngraded": "O seu plano foi rebaixado com sucesso", + "yourSubscriptionWasUpdatedSuccessfully": "A sua subscrição foi actualizada com sucesso", "googlePlayId": "ID do Google Play", "appleId": "ID da Apple", - "playstoreSubscription": "Assinatura da PlayStore", - "appstoreSubscription": "Assinatura da AppStore", - "subAlreadyLinkedErrMessage": "Seu {id} já está vinculado a outra conta Ente. Se você gostaria de usar seu {id} com esta conta, entre em contato conosco\"", - "visitWebToManage": "Visite o web.ente.io para gerenciar sua assinatura", - "couldNotUpdateSubscription": "Não foi possível atualizar a assinatura", - "pleaseContactSupportAndWeWillBeHappyToHelp": "Entre em contato com support@ente.io e nós ficaremos felizes em ajudar!", + "playstoreSubscription": "Subscrição da PlayStore", + "appstoreSubscription": "Subscrição da AppStore", + "subAlreadyLinkedErrMessage": "Seu {id} já está vinculado a outra conta Ente.\nSe você gostaria de usar seu {id} com esta conta, por favor contate nosso suporte''", + "visitWebToManage": "Visite web.ente.io para gerir a sua subscrição", + "couldNotUpdateSubscription": "Não foi possível atualizar a subscrição", + "pleaseContactSupportAndWeWillBeHappyToHelp": "Por favor, entre em contato com support@ente.io e nós ficaremos felizes em ajudar!", "paymentFailed": "O pagamento falhou", - "paymentFailedTalkToProvider": "Fale com o suporte {providerName} se você foi cobrado", + "paymentFailedTalkToProvider": "Por favor, fale com o suporte {providerName} se você foi cobrado", "@paymentFailedTalkToProvider": { "description": "The text to display when the payment failed", "type": "text", @@ -678,41 +656,39 @@ } } }, - "continueOnFreeTrial": "Continuar com a avaliação grátis", - "areYouSureYouWantToExit": "Tem certeza de que queira sair?", + "continueOnFreeTrial": "Continuar em teste gratuito", + "areYouSureYouWantToExit": "Tem certeza de que deseja sair?", "thankYou": "Obrigado", - "failedToVerifyPaymentStatus": "Falhou ao verificar estado do pagamento", - "pleaseWaitForSometimeBeforeRetrying": "Por favor, aguarde mais algum tempo antes de tentar novamente", - "paymentFailedMessage": "Infelizmente o pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!", + "failedToVerifyPaymentStatus": "Falha ao verificar status do pagamento", + "pleaseWaitForSometimeBeforeRetrying": "Por favor, aguarde algum tempo antes de tentar novamente", + "paymentFailedMessage": "Infelizmente o seu pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!", "youAreOnAFamilyPlan": "Você está em um plano familiar!", - "contactFamilyAdmin": "Entre em contato com {familyAdminEmail} para gerenciar sua assinatura", - "leaveFamily": "Sair do plano familiar", - "areYouSureThatYouWantToLeaveTheFamily": "Você tem certeza que queira sair do plano familiar?", + "contactFamilyAdmin": "Contacte {familyAdminEmail} para gerir a sua subscrição", + "leaveFamily": "Deixar plano famíliar", + "areYouSureThatYouWantToLeaveTheFamily": "Tem certeza que deseja sair do plano familiar?", "leave": "Sair", - "rateTheApp": "Avalie o aplicativo", + "rateTheApp": "Avaliar aplicação", "startBackup": "Iniciar cópia de segurança", - "noPhotosAreBeingBackedUpRightNow": "No momento não há fotos sendo copiadas com segurança", + "noPhotosAreBeingBackedUpRightNow": "No momento não há backup de fotos sendo feito", "preserveMore": "Preservar mais", - "grantFullAccessPrompt": "Permita o acesso a todas as fotos nas opções do aplicativo", - "allowPermTitle": "Permita acesso às Fotos", - "allowPermBody": "Permita o acesso a suas fotos das Configurações para que Ente possa exibir e copiar com segurança sua biblioteca.", - "openSettings": "Abrir opções", + "grantFullAccessPrompt": "Por favor, permita o acesso a todas as fotos nas definições do aplicativo", + "openSettings": "Abrir Definições", "selectMorePhotos": "Selecionar mais fotos", - "existingUser": "Usuário existente", - "privateBackups": "Cópias privadas", + "existingUser": "Utilizador existente", + "privateBackups": "Backups privados", "forYourMemories": "para suas memórias", - "endtoendEncryptedByDefault": "Criptografado de ponta a ponta por padrão", + "endtoendEncryptedByDefault": "Criptografia de ponta a ponta por padrão", "safelyStored": "Armazenado com segurança", "atAFalloutShelter": "em um abrigo avançado", "designedToOutlive": "Feito para ter longevidade", "available": "Disponível", - "everywhere": "em todas as partes", - "androidIosWebDesktop": "Android, iOS, Web, Computador", - "mobileWebDesktop": "Celular, Web, Computador", + "everywhere": "em todo o lado", + "androidIosWebDesktop": "Android, iOS, Web, Desktop", + "mobileWebDesktop": "Mobile, Web, Desktop", "newToEnte": "Novo no Ente", - "pleaseLoginAgain": "Registre-se novamente", - "autoLogoutMessage": "Devido ao ocorrido de erros técnicos, você foi desconectado. Pedimos desculpas pela inconveniência.", - "yourSubscriptionHasExpired": "A sua assinatura expirou", + "pleaseLoginAgain": "Por favor, inicie sessão novamente", + "autoLogoutMessage": "Devido a uma falha técnica, a sua sessão foi encerrada. Pedimos desculpas pelo incómodo.", + "yourSubscriptionHasExpired": "A sua subscrição expirou", "storageLimitExceeded": "Limite de armazenamento excedido", "upgrade": "Atualizar", "raiseTicket": "Abrir ticket", @@ -720,19 +696,19 @@ "description": "Button text for raising a support tickets in case of unhandled errors during backup", "type": "text" }, - "backupFailed": "Falhou ao copiar com segurança", - "couldNotBackUpTryLater": "Nós não podemos copiar com segurança seus dados.\nNós tentaremos novamente mais tarde.", + "backupFailed": "Backup falhou", + "couldNotBackUpTryLater": "Não foi possível fazer o backup de seus dados.\nTentaremos novamente mais tarde.", "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente pode criptografar e preservar arquivos apenas se você conceder acesso a eles", "pleaseGrantPermissions": "Por favor, conceda as permissões", - "grantPermission": "Conceder permissões", - "privateSharing": "Compartilhamento privado", - "shareOnlyWithThePeopleYouWant": "Compartilhar apenas com as pessoas que você quiser", + "grantPermission": "Conceder permissão", + "privateSharing": "Partilha privada", + "shareOnlyWithThePeopleYouWant": "Partilhar apenas com as pessoas que deseja", "usePublicLinksForPeopleNotOnEnte": "Usar links públicos para pessoas que não estão no Ente", - "allowPeopleToAddPhotos": "Permitir que pessoas adicionem fotos", - "shareAnAlbumNow": "Compartilhar um álbum agora", - "collectEventPhotos": "Coletar fotos de evento", + "allowPeopleToAddPhotos": "Permitir que as pessoas adicionem fotos", + "shareAnAlbumNow": "Partilhar um álbum", + "collectEventPhotos": "Coletar fotos do evento", "sessionExpired": "Sessão expirada", - "loggingOut": "Desconectando...", + "loggingOut": "Terminar a sessão...", "@onDevice": { "description": "The text displayed above folders/albums stored on device", "type": "text" @@ -742,14 +718,14 @@ "description": "The text displayed above albums backed up to Ente", "type": "text" }, - "onEnte": "No ente", + "onEnte": "Em ente", "name": "Nome", - "newest": "Mais recente", + "newest": "Recentes", "lastUpdated": "Última atualização", - "deleteEmptyAlbums": "Excluir álbuns vazios", - "deleteEmptyAlbumsWithQuestionMark": "Excluir álbuns vazios?", - "deleteAlbumsDialogBody": "Isso excluirá todos os álbuns vazios. Isso é útil quando você quiser reduzir a desordem no seu álbum.", - "deleteProgress": "Excluindo {currentlyDeleting} / {totalCount}", + "deleteEmptyAlbums": "Apagar álbuns vazios", + "deleteEmptyAlbumsWithQuestionMark": "Apagar álbuns vazios?", + "deleteAlbumsDialogBody": "Esta ação elimina todos os álbuns vazios. Isto é útil quando pretende reduzir a confusão na sua lista de álbuns.", + "deleteProgress": "Apagar {currentlyDeleting} / {totalCount}", "genericProgress": "Processando {currentlyProcessing} / {totalCount}", "@genericProgress": { "description": "Generic progress text to display when processing multiple items", @@ -765,66 +741,58 @@ } } }, - "permanentlyDelete": "Excluir permanentemente", - "canOnlyCreateLinkForFilesOwnedByYou": "Só é possível criar um link para arquivos pertencentes a você", + "permanentlyDelete": "Eliminar permanentemente", + "canOnlyCreateLinkForFilesOwnedByYou": "Só pode criar um link para arquivos pertencentes a você", "publicLinkCreated": "Link público criado", - "youCanManageYourLinksInTheShareTab": "Você pode gerenciar seus links na aba de compartilhamento.", + "youCanManageYourLinksInTheShareTab": "Pode gerir as suas ligações no separador partilhar.", "linkCopiedToClipboard": "Link copiado para a área de transferência", "restore": "Restaurar", "@restore": { "description": "Display text for an action which triggers a restore of item from trash", "type": "text" }, - "moveToAlbum": "Mover para o álbum", - "unhide": "Desocultar", + "moveToAlbum": "Mover para álbum", + "unhide": "Mostrar", "unarchive": "Desarquivar", "favorite": "Favorito", - "removeFromFavorite": "Desfavoritar", - "shareLink": "Compartilhar link", - "createCollage": "Criar colagem", - "saveCollage": "Salvar colagem", - "collageSaved": "Colagem salva na galeria", + "removeFromFavorite": "Remover dos favoritos", + "shareLink": "Partilhar link", + "createCollage": "Criar coleção", + "saveCollage": "Guardar colagem", + "collageSaved": "Colagem guardada na galeria", "collageLayout": "Layout", "addToEnte": "Adicionar ao Ente", "addToAlbum": "Adicionar ao álbum", - "delete": "Excluir", + "delete": "Apagar", "hide": "Ocultar", - "share": "Compartilhar", - "unhideToAlbum": "Desocultar para o álbum", + "share": "Partilhar", + "unhideToAlbum": "Mostrar para o álbum", "restoreToAlbum": "Restaurar para álbum", - "moveItem": "{count, plural, one {Mover item} other {Mover itens}}", - "@moveItem": { - "description": "Page title while moving one or more items to an album" - }, - "addItem": "{count, plural, one {Adicionar item} other {Adicionar itens}}", - "@addItem": { - "description": "Page title while adding one or more items to album" - }, "createOrSelectAlbum": "Criar ou selecionar álbum", "selectAlbum": "Selecionar álbum", "searchByAlbumNameHint": "Nome do álbum", "albumTitle": "Título do álbum", - "enterAlbumName": "Inserir nome do álbum", - "restoringFiles": "Restaurando arquivos...", - "movingFilesToAlbum": "Movendo arquivos para o álbum...", - "unhidingFilesToAlbum": "Desocultando arquivos para o álbum", - "canNotUploadToAlbumsOwnedByOthers": "Não é possível enviar para álbuns pertencentes a outros", - "uploadingFilesToAlbum": "Enviando arquivos para o álbum...", - "addedSuccessfullyTo": "Adicionado com sucesso a {albumName}", + "enterAlbumName": "Introduzir nome do álbum", + "restoringFiles": "Restaurar arquivos...", + "movingFilesToAlbum": "Mover arquivos para o álbum...", + "unhidingFilesToAlbum": "Desocultar ficheiros para o álbum", + "canNotUploadToAlbumsOwnedByOthers": "Não é possível fazer upload para álbuns pertencentes a outros", + "uploadingFilesToAlbum": "Enviar ficheiros para o álbum...", + "addedSuccessfullyTo": "Adicionado com sucesso a {albumName}", "movedSuccessfullyTo": "Movido com sucesso para {albumName}", "thisAlbumAlreadyHDACollaborativeLink": "Este álbum já tem um link colaborativo", "collaborativeLinkCreatedFor": "Link colaborativo criado para {albumName}", - "askYourLovedOnesToShare": "Peça que seus entes queridos compartilhem", + "askYourLovedOnesToShare": "Peça aos seus entes queridos para partilharem", "invite": "Convidar", - "shareYourFirstAlbum": "Compartilhar seu primeiro álbum", - "sharedWith": "Compartilhado com {emailIDs}", - "sharedWithMe": "Compartilhado comigo", - "sharedByMe": "Compartilhada por mim", - "doubleYourStorage": "Duplique seu armazenamento", - "referFriendsAnd2xYourPlan": "Recomende seus amigos e duplique seu plano", - "shareAlbumHint": "Abra um álbum e toque no botão compartilhar no canto superior direito para compartilhar.", - "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": "Os itens exibem o número de dias restantes antes da exclusão permanente", - "trashDaysLeft": "{count, plural, =0 {Em breve} =1 {1 dia} other {{count} dias}}", + "shareYourFirstAlbum": "Partilhe o seu primeiro álbum", + "sharedWith": "Partilhado com {emailIDs}", + "sharedWithMe": "Partilhado comigo", + "sharedByMe": "Partilhado por mim", + "doubleYourStorage": "Duplicar o seu armazenamento", + "referFriendsAnd2xYourPlan": "Recomende amigos e duplique o seu plano", + "shareAlbumHint": "Abra um álbum e toque no botão de partilha no canto superior direito para partilhar", + "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": "Os itens mostram o número de dias restantes antes da eliminação permanente", + "trashDaysLeft": "{count, plural, =0 {Brevemente} =1 {1 dia} other {{count} dias}}", "@trashDaysLeft": { "description": "Text to indicate number of days remaining before permanent deletion", "placeholders": { @@ -834,7 +802,7 @@ } } }, - "deleteAll": "Excluir tudo", + "deleteAll": "Apagar tudo", "renameAlbum": "Renomear álbum", "convertToAlbum": "Converter para álbum", "setCover": "Definir capa", @@ -842,63 +810,62 @@ "description": "Text to set cover photo for an album" }, "sortAlbumsBy": "Ordenar por", - "sortNewestFirst": "Recentes primeiro", - "sortOldestFirst": "Antigos primeiro", + "sortNewestFirst": "Mais recentes primeiro", + "sortOldestFirst": "Mais antigos primeiro", "rename": "Renomear", "leaveSharedAlbum": "Sair do álbum compartilhado?", "leaveAlbum": "Sair do álbum", - "photosAddedByYouWillBeRemovedFromTheAlbum": "Suas fotos adicionadas serão removidas do álbum", - "youveNoFilesInThisAlbumThatCanBeDeleted": "Você não tem arquivos neste álbum que possam ser excluídos", - "youDontHaveAnyArchivedItems": "Você não tem nenhum item arquivado.", - "ignoredFolderUploadReason": "Alguns arquivos neste álbum são ignorados do envio porque eles foram anteriormente excluídos do Ente.", - "resetIgnoredFiles": "Redefinir arquivos ignorados", - "deviceFilesAutoUploading": "Arquivos adicionados ao álbum do dispositivo serão automaticamente enviados para o Ente.", - "turnOnBackupForAutoUpload": "Ative a cópia de segurança para automaticamente enviar arquivos adicionados à pasta do dispositivo para o Ente.", + "photosAddedByYouWillBeRemovedFromTheAlbum": "As fotos adicionadas por si serão removidas do álbum", + "youveNoFilesInThisAlbumThatCanBeDeleted": "Não existem ficheiros neste álbum que possam ser eliminados", + "youDontHaveAnyArchivedItems": "Não tem nenhum item arquivado.", + "ignoredFolderUploadReason": "Alguns ficheiros deste álbum não podem ser carregados porque foram anteriormente eliminados do Ente.", + "resetIgnoredFiles": "Repor ficheiros ignorados", + "deviceFilesAutoUploading": "Os ficheiros adicionados a este álbum de dispositivo serão automaticamente transferidos para o Ente.", + "turnOnBackupForAutoUpload": "Ative o backup para enviar automaticamente arquivos adicionados a esta pasta do dispositivo para o Ente.", "noHiddenPhotosOrVideos": "Sem fotos ou vídeos ocultos", - "toHideAPhotoOrVideo": "Para ocultar uma foto ou vídeo", - "openTheItem": "• Abra a foto ou vídeo", + "toHideAPhotoOrVideo": "Para ocultar uma foto ou um vídeo", + "openTheItem": "• Abra o item", "clickOnTheOverflowMenu": "• Clique no menu adicional", - "click": "• Clique", + "click": "Clique", "nothingToSeeHere": "Nada para ver aqui! 👀", "unarchiveAlbum": "Desarquivar álbum", "archiveAlbum": "Arquivar álbum", - "calculating": "Calculando...", - "pleaseWaitDeletingAlbum": "Aguarde, excluindo álbum", - "searchByExamples": "• Nomes de álbuns (ex: \"Câmera\")\n• Tipos de arquivos (ex.: \"Vídeos\", \".gif\")\n• Anos e meses (ex.: \"2022\", \"Janeiro\")\n• Temporadas (ex.: \"Natal\")\n• Tags (ex.: \"#divertido\")", - "youCanTrySearchingForADifferentQuery": "Você pode tentar buscar por outra consulta.", - "noResultsFound": "Nenhum resultado encontrado", + "calculating": "Calcular...", + "pleaseWaitDeletingAlbum": "Por favor aguarde, apagar o álbum", + "searchByExamples": "• Nomes de álbuns (ex: \"Câmera\")\n• Tipos de arquivos (ex.: \"Vídeos\", \".gif\")\n• Anos e meses (e.. \"2022\", \"Janeiro\")\n• Feriados (por exemplo, \"Natal\")\n• Descrições de fotos (por exemplo, \"#divertido\")", + "youCanTrySearchingForADifferentQuery": "Pode tentar pesquisar uma consulta diferente.", + "noResultsFound": "Não foram encontrados resultados", "addedBy": "Adicionado por {emailOrName}", "loadingExifData": "Carregando dados EXIF...", "viewAllExifData": "Ver todos os dados EXIF", "noExifData": "Sem dados EXIF", - "thisImageHasNoExifData": "Esta imagem não possui dados EXIF", + "thisImageHasNoExifData": "Esta imagem não tem dados exif", "exif": "EXIF", "noResults": "Nenhum resultado", - "weDontSupportEditingPhotosAndAlbumsThatYouDont": "Não suportamos a edição de fotos e álbuns que você ainda não possui", - "failedToFetchOriginalForEdit": "Falhou ao obter original para edição", + "weDontSupportEditingPhotosAndAlbumsThatYouDont": "Não suportamos a edição de fotos e álbuns que ainda não possui", + "failedToFetchOriginalForEdit": "Falha ao obter original para edição", "close": "Fechar", "setAs": "Definir como", - "fileSavedToGallery": "Arquivo salvo na galeria", - "filesSavedToGallery": "Arquivos salvos na galeria", - "fileFailedToSaveToGallery": "Falhou ao salvar arquivo na galeria", - "download": "Baixar", + "fileSavedToGallery": "Arquivo guardado na galeria", + "filesSavedToGallery": "Arquivos guardados na galeria", + "fileFailedToSaveToGallery": "Falha ao guardar o ficheiro na galeria", + "download": "Download", "pressAndHoldToPlayVideo": "Pressione e segure para reproduzir o vídeo", "pressAndHoldToPlayVideoDetailed": "Pressione e segure na imagem para reproduzir o vídeo", - "downloadFailed": "Falhou ao baixar", + "downloadFailed": "Falha no download", "deduplicateFiles": "Arquivos duplicados", - "deselectAll": "Deselecionar tudo", - "reviewDeduplicateItems": "Reveja e exclua os itens que você acredita serem duplicados.", + "deselectAll": "Desmarcar tudo", + "reviewDeduplicateItems": "Reveja e elimine os itens que considera serem duplicados.", "clubByCaptureTime": "Agrupar por tempo de captura", - "clubByFileName": "Agrupar por nome do arquivo", + "clubByFileName": "Agrupar pelo nome de arquivo", "count": "Contagem", "totalSize": "Tamanho total", - "longpressOnAnItemToViewInFullscreen": "Mantenha pressionado em um item para visualizá-lo em tela cheia", + "longpressOnAnItemToViewInFullscreen": "Pressione e segure em um item para ver em tela cheia", "decryptingVideo": "Descriptografando vídeo...", - "authToViewYourMemories": "Autentique-se para ver suas memórias", + "authToViewYourMemories": "Por favor, autentique-se para ver suas memórias", "unlock": "Desbloquear", - "freeUpSpace": "Liberar espaço", - "freeUpSpaceSaving": "{count, plural, one {Ele pode ser excluído do dispositivo para liberar {formattedSize}} other {Eles podem ser excluídos do dispositivo para liberar {formattedSize}}}", - "filesBackedUpInAlbum": "{count, plural, one {1 arquivo} other {{formattedNumber} arquivos}} deste álbum foi copiado com segurança", + "freeUpSpace": "Libertar espaço", + "filesBackedUpInAlbum": "{count, plural, one {1 arquivo} other {{formattedNumber} arquivos}} neste álbum teve um backup seguro", "@filesBackedUpInAlbum": { "description": "Text to tell user how many files have been backed up in the album", "placeholders": { @@ -913,7 +880,7 @@ } } }, - "filesBackedUpFromDevice": "{count, plural, one {1 arquivo} other {{formattedNumber} arquivos}} deste dispositivo foi copiado com segurança", + "filesBackedUpFromDevice": "{count, plural, one {1 arquivo} other {{formattedNumber} arquivos}} neste dispositivo teve um backup seguro", "@filesBackedUpFromDevice": { "description": "Text to tell user how many files have been backed up from this device", "placeholders": { @@ -928,43 +895,31 @@ } } }, - "@freeUpSpaceSaving": { - "description": "Text to tell user how much space they can free up by deleting items from the device" - }, - "freeUpAccessPostDelete": "Você ainda pode acessá-{count, plural, one {lo} other {los}} no Ente, contanto que você tenha uma assinatura ativa", - "@freeUpAccessPostDelete": { - "placeholders": { - "count": { - "example": "1", - "type": "int" - } - } - }, - "freeUpAmount": "Liberar {sizeInMBorGB}", - "thisEmailIsAlreadyInUse": "Este e-mail já está sendo usado", - "incorrectCode": "Código incorreto", - "authenticationFailedPleaseTryAgain": "Falha na autenticação. Tente novamente", - "verificationFailedPleaseTryAgain": "Falha na verificação. Tente novamente", - "authenticating": "Autenticando...", - "authenticationSuccessful": "Autenticado com sucesso!", + "freeUpAmount": "Libertar {sizeInMBorGB}", + "thisEmailIsAlreadyInUse": "Este email já está em uso", + "incorrectCode": "Código incorrecto", + "authenticationFailedPleaseTryAgain": "Falha na autenticação, por favor tente novamente", + "verificationFailedPleaseTryAgain": "Falha na verificação, por favor tente novamente", + "authenticating": "A Autenticar...", + "authenticationSuccessful": "Autenticação bem sucedida!", "incorrectRecoveryKey": "Chave de recuperação incorreta", "theRecoveryKeyYouEnteredIsIncorrect": "A chave de recuperação inserida está incorreta", - "twofactorAuthenticationSuccessfullyReset": "Autenticação de dois fatores redefinida com sucesso", - "pleaseVerifyTheCodeYouHaveEntered": "Verifique o código inserido", + "twofactorAuthenticationSuccessfullyReset": "Autenticação de dois fatores redefinida com êxito", + "pleaseVerifyTheCodeYouHaveEntered": "Por favor, verifique se o código que você inseriu", "pleaseContactSupportIfTheProblemPersists": "Por favor, contate o suporte se o problema persistir", "twofactorAuthenticationHasBeenDisabled": "A autenticação de dois fatores foi desativada", - "sorryTheCodeYouveEnteredIsIncorrect": "O código inserido está incorreto", - "yourVerificationCodeHasExpired": "O código de verificação expirou", - "emailChangedTo": "E-mail alterado para {newEmail}", - "verifying": "Verificando...", - "disablingTwofactorAuthentication": "Desativando a autenticação de dois fatores...", + "sorryTheCodeYouveEnteredIsIncorrect": "Desculpe, o código inserido está incorreto", + "yourVerificationCodeHasExpired": "O seu código de verificação expirou", + "emailChangedTo": "Email alterado para {newEmail}", + "verifying": "A verificar…", + "disablingTwofactorAuthentication": "Desativar a autenticação de dois factores...", "allMemoriesPreserved": "Todas as memórias preservadas", "loadingGallery": "Carregando galeria...", "syncing": "Sincronizando...", - "encryptingBackup": "Criptografando cópia de segurança...", + "encryptingBackup": "Criptografando backup...", "syncStopped": "Sincronização interrompida", "syncProgress": "{completed}/{total} memórias preservadas", - "uploadingMultipleMemories": "Preservando {count} memórias...", + "uploadingMultipleMemories": "Preservar {count} memórias...", "@uploadingMultipleMemories": { "description": "Text to tell user how many memories are being preserved", "placeholders": { @@ -973,7 +928,7 @@ } } }, - "uploadingSingleMemory": "Preservando 1 memória...", + "uploadingSingleMemory": "Preservar 1 memória...", "@syncProgress": { "description": "Text to tell user how many memories have been preserved", "placeholders": { @@ -985,100 +940,99 @@ } } }, - "archiving": "Arquivando...", - "unarchiving": "Desarquivando...", + "archiving": "Arquivar...", + "unarchiving": "Desarquivar...", "successfullyArchived": "Arquivado com sucesso", "successfullyUnarchived": "Desarquivado com sucesso", "renameFile": "Renomear arquivo", "enterFileName": "Inserir nome do arquivo", - "filesDeleted": "Arquivos excluídos", + "filesDeleted": "Arquivos apagados", "selectedFilesAreNotOnEnte": "Os arquivos selecionados não estão no Ente", "thisActionCannotBeUndone": "Esta ação não pode ser desfeita", - "emptyTrash": "Esvaziar a lixeira?", - "permDeleteWarning": "Todos os itens na lixeira serão excluídos permanentemente\n\nEsta ação não pode ser desfeita", + "emptyTrash": "Esvaziar lixo?", + "permDeleteWarning": "Todos os itens no lixo serão permanentemente eliminados\n\n\nEsta ação não pode ser anulada", "empty": "Esvaziar", - "couldNotFreeUpSpace": "Não foi possível liberar espaço", - "permanentlyDeleteFromDevice": "Excluir permanentemente do dispositivo?", - "someOfTheFilesYouAreTryingToDeleteAre": "Alguns dos arquivos que você está tentando excluir só estão disponíveis no seu dispositivo e não podem ser recuperados se forem excluídos", - "theyWillBeDeletedFromAllAlbums": "Eles serão excluídos de todos os álbuns.", - "someItemsAreInBothEnteAndYourDevice": "Alguns itens estão em ambos o Ente quanto no seu dispositivo.", - "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": "Os itens selecionados serão excluídos de todos os álbuns e movidos para a lixeira.", - "theseItemsWillBeDeletedFromYourDevice": "Estes itens serão excluídos do seu dispositivo.", - "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Parece que algo deu errado. Tente novamente mais tarde. Caso o erro persistir, por favor, entre em contato com nossa equipe.", + "couldNotFreeUpSpace": "Não foi possível libertar espaço", + "permanentlyDeleteFromDevice": "Apagar permanentemente do dispositivo?", + "someOfTheFilesYouAreTryingToDeleteAre": "Alguns dos ficheiros que está a tentar eliminar só estão disponíveis no seu dispositivo e não podem ser recuperados se forem eliminados", + "theyWillBeDeletedFromAllAlbums": "Serão eliminados de todos os álbuns.", + "someItemsAreInBothEnteAndYourDevice": "Alguns itens estão tanto no Ente como no seu dispositivo.", + "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": "Os itens selecionados serão eliminados de todos os álbuns e movidos para o lixo.", + "theseItemsWillBeDeletedFromYourDevice": "Estes itens serão eliminados do seu dispositivo.", + "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Parece que algo correu mal. Por favor, tente novamente após algum tempo. Se o erro persistir, contacte a nossa equipa de apoio.", "error": "Erro", - "tempErrorContactSupportIfPersists": "Parece que algo deu errado. Tente novamente mais tarde. Caso o erro persistir, por favor, entre em contato com nossa equipe.", - "networkHostLookUpErr": "Não foi possível conectar-se ao Ente, verifique suas configurações de rede e entre em contato com o suporte se o erro persistir.", - "networkConnectionRefusedErr": "Não foi possível conectar ao Ente, tente novamente mais tarde. Se o erro persistir, entre em contato com o suporte.", - "cachedData": "Dados armazenados em cache", + "tempErrorContactSupportIfPersists": "Parece que algo correu mal. Por favor, tente novamente mais tarde. Se o erro persistir, entre em contacto com a nossa equipa de suporte.", + "networkHostLookUpErr": "Não foi possível estabelecer ligação ao Ente. Verifique as definições de rede e contacte o serviço de apoio se o erro persistir.", + "networkConnectionRefusedErr": "Não foi possível conectar ao Ente, tente novamente após algum tempo. Se o erro persistir, entre em contato com o suporte.", + "cachedData": "Dados em cache", "clearCaches": "Limpar cache", "remoteImages": "Imagens remotas", "remoteVideos": "Vídeos remotos", "remoteThumbnails": "Miniaturas remotas", "pendingSync": "Sincronização pendente", "localGallery": "Galeria local", - "todaysLogs": "Registros de hoje", - "viewLogs": "Ver registros", - "logsDialogBody": "Isso enviará através dos registros para ajudar-nos a resolver seu problema. Saiba que, nome de arquivos serão incluídos para ajudar a buscar problemas com arquivos específicos.", - "preparingLogs": "Preparando registros...", - "emailYourLogs": "Enviar registros por e-mail", - "pleaseSendTheLogsTo": "Envie os registros para \n{toEmail}", - "copyEmailAddress": "Copiar endereço de e-mail", - "exportLogs": "Exportar registros", - "pleaseEmailUsAt": "Envie-nos um e-mail para {toEmail}", - "dismiss": "Descartar", + "todaysLogs": "Logs de hoje", + "viewLogs": "Ver logs", + "logsDialogBody": "Isto enviará os registos para nos ajudar a resolver o problema. Tenha em atenção que os nomes dos ficheiros serão incluídos para ajudar a localizar problemas com ficheiros específicos.", + "preparingLogs": "Preparando logs...", + "emailYourLogs": "Enviar logs por e-mail", + "pleaseSendTheLogsTo": "Por favor, envie os logs para \n{toEmail}", + "copyEmailAddress": "Copiar endereço de email", + "exportLogs": "Exportar logs", + "pleaseEmailUsAt": "Por favor, envie-nos um e-mail para {toEmail}", + "dismiss": "Rejeitar", "didYouKnow": "Você sabia?", - "loadingMessage": "Carregando suas fotos...", - "loadMessage1": "Você pode compartilhar sua assinatura com seus familiares", - "loadMessage2": "Nós preservamos mais de 30 milhões de memórias até então", + "loadingMessage": "Carregar as suas fotos...", + "loadMessage1": "Pode partilhar a sua subscrição com a sua família", "loadMessage3": "Mantemos 3 cópias dos seus dados, uma em um abrigo subterrâneo", "loadMessage4": "Todos os nossos aplicativos são de código aberto", "loadMessage5": "Nosso código-fonte e criptografia foram auditadas externamente", - "loadMessage6": "Você pode compartilhar links para seus álbuns com seus entes queridos", - "loadMessage7": "Nossos aplicativos móveis são executados em segundo plano para criptografar e copiar com segurança quaisquer fotos novas que você acessar", - "loadMessage8": "web.ente.io tem um enviador mais rápido", + "loadMessage6": "Deixar o álbum partilhado?", + "loadMessage7": "Nossos aplicativos móveis são executados em segundo plano para criptografar e fazer backup de quaisquer novas fotos que você clique", + "loadMessage8": "web.ente.io tem um envio mais rápido", "loadMessage9": "Nós usamos Xchacha20Poly1305 para criptografar seus dados com segurança", "photoDescriptions": "Descrições das fotos", "fileTypesAndNames": "Tipos de arquivo e nomes", "location": "Localização", "moments": "Momentos", - "searchFaceEmptySection": "As pessoas apareceram aqui quando a indexação for concluída", - "searchDatesEmptySection": "Buscar por data, mês ou ano", + "searchFaceEmptySection": "As pessoas serão mostradas aqui quando a indexação estiver concluída", + "searchDatesEmptySection": "Pesquisar por data, mês ou ano", "searchLocationEmptySection": "Fotos de grupo que estão sendo tiradas em algum raio da foto", - "searchPeopleEmptySection": "Convide pessoas e você verá todas as fotos compartilhadas por elas aqui", + "searchPeopleEmptySection": "Convide pessoas e verá todas as fotos partilhadas por elas aqui", "searchAlbumsEmptySection": "Álbuns", "searchFileTypesAndNamesEmptySection": "Tipos de arquivo e nomes", - "searchCaptionEmptySection": "Adicione marcações como \"#viagem\" nas informações das fotos para encontrá-las aqui com facilidade", + "searchCaptionEmptySection": "Adicione descrições como \"#trip\" nas informações das fotos para encontrá-las aqui rapidamente", "language": "Idioma", - "selectLanguage": "Selecionar idioma", + "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 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", + "locationTagFeatureDescription": "Uma etiqueta de localização agrupa todas as fotos que foram tiradas num determinado raio de uma fotografia", + "galleryMemoryLimitInfo": "Até 1000 memórias mostradas na galeria", + "save": "Guardar", "centerPoint": "Ponto central", "pickCenterPoint": "Escolha o ponto central", - "useSelectedPhoto": "Usar foto selecionada", + "useSelectedPhoto": "Utilizar foto selecionada", "resetToDefault": "Redefinir para o padrão", "@resetToDefault": { "description": "Button text to reset cover photo to default" }, "edit": "Editar", - "deleteLocation": "Excluir localização", - "rotateLeft": "Girar para a esquerda", + "deleteLocation": "Apagar localização", + "rotateLeft": "Rodar para a esquerda", "flip": "Inverter", - "rotateRight": "Girar para a direita", - "saveCopy": "Salvar cópia", - "light": "Brilho", + "rotateRight": "Rodar para a direita", + "saveCopy": "Guardar cópia", + "light": "Claro", "color": "Cor", - "yesDiscardChanges": "Sim, descartar alterações", - "doYouWantToDiscardTheEditsYouHaveMade": "Você quer descartar as edições que você fez?", - "saving": "Salvando...", - "editsSaved": "Edições salvas", - "oopsCouldNotSaveEdits": "Opa! Não foi possível salvar as edições", + "yesDiscardChanges": "Sim, rejeitar alterações", + "doYouWantToDiscardTheEditsYouHaveMade": "Pretende eliminar as edições que efectuou?", + "saving": "A gravar...", + "editsSaved": "Edição guardada", + "oopsCouldNotSaveEdits": "Oops, não foi possível guardar as edições", "distanceInKMUnit": "km", "@distanceInKMUnit": { "description": "Unit for distance in km" @@ -1086,9 +1040,9 @@ "dayToday": "Hoje", "dayYesterday": "Ontem", "storage": "Armazenamento", - "usedSpace": "Espaço usado", + "usedSpace": "Espaço utilizado", "storageBreakupFamily": "Família", - "storageBreakupYou": "Você", + "storageBreakupYou": "Tu", "@storageBreakupYou": { "description": "Label to indicate how much storage you are using when you are part of a family plan" }, @@ -1096,10 +1050,10 @@ "@storageUsageInfo": { "description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used" }, - "availableStorageSpace": "{freeAmount} {storageUnit} livre", + "availableStorageSpace": "{freeAmount} {storageUnit} grátis", "appVersion": "Versão: {versionValue}", "verifyIDLabel": "Verificar", - "fileInfoAddDescHint": "Adicionar descrição...", + "fileInfoAddDescHint": "Acrescente uma descrição...", "editLocationTagTitle": "Editar localização", "setLabel": "Definir", "@setLabel": { @@ -1107,7 +1061,7 @@ }, "setRadius": "Definir raio", "familyPlanPortalTitle": "Família", - "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!", + "familyPlanOverview": "Adicione 5 membros da família ao seu plano existente sem pagar mais.\n\n\nCada membro tem o seu próprio espaço privado e não pode ver os ficheiros dos outros, a menos que sejam partilhados.\n\n\nOs planos familiares estão disponíveis para clientes que tenham uma subscrição paga do Ente.\n\n\nSubscreva agora para começar!", "androidBiometricHint": "Verificar identidade", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." @@ -1128,31 +1082,31 @@ "@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." }, - "androidBiometricRequiredTitle": "Biométrica necessária", + "androidBiometricRequiredTitle": "Biometria necessária", "@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 necessários", + "androidDeviceCredentialsRequiredTitle": "Credenciais do dispositivo são necessárias", "@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 necessários", + "androidDeviceCredentialsSetupDescription": "Credenciais do dispositivo necessárias", "@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 às opções", + "goToSettings": "Ir para as definiçõ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á definida no dispositivo. Vá em 'Opções > Segurança' para adicionar a autenticação biométrica.", + "androidGoToSettingsDescription": "A autenticação biométrica não está configurada no seu dispositivo. Vá a “Definiçõ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. Bloqueie e desbloqueie sua tela para ativá-la.", + "iOSLockOut": "A autenticação biométrica está desativada. Por favor, bloqueie e desbloqueie o ecrã 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á definida no dispositivo. Ative o Touch ID ou Face ID no dispositivo.", + "iOSGoToSettingsDescription": "A autenticação biométrica não está configurada no seu dispositivo. Active o Touch ID ou o Face ID no seu telemóvel.", "@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." }, @@ -1161,61 +1115,61 @@ "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 em OSM France", + "hostedAtOsmFrance": "Hospedado na OSM France", "map": "Mapa", "@map": { "description": "Label for the map view" }, "maps": "Mapas", "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.", + "enableMapsDesc": "Esta opção mostra as suas fotografias num mapa do mundo.\n\n\nEste mapa é alojado pelo Open Street Map e as localizações exactas das suas fotografias nunca são partilhadas.\n\n\nPode desativar esta funcionalidade em qualquer altura nas Definições.", "quickLinks": "Links rápidos", "selectItemsToAdd": "Selecionar itens para adicionar", - "addSelected": "Adicionar selecionado", - "addFromDevice": "Adicionar do dispositivo", + "addSelected": "Adicionar selecionados", + "addFromDevice": "Adicionar a partir do dispositivo", "addPhotos": "Adicionar fotos", "noPhotosFoundHere": "Nenhuma foto encontrada aqui", - "zoomOutToSeePhotos": "Reduzir ampliação para ver as fotos", + "zoomOutToSeePhotos": "Diminuir o zoom para ver fotos", "noImagesWithLocation": "Nenhuma imagem com localização", "unpinAlbum": "Desafixar álbum", "pinAlbum": "Fixar álbum", "create": "Criar", "viewAll": "Ver tudo", - "nothingSharedWithYouYet": "Nada compartilhado com você ainda", - "noAlbumsSharedByYouYet": "Nenhum álbum compartilhado por você ainda", - "sharedWithYou": "Compartilhado com você", - "sharedByYou": "Compartilhado por você", - "inviteYourFriendsToEnte": "Convide seus amigos ao Ente", - "failedToDownloadVideo": "Falhou ao baixar vídeo", + "nothingSharedWithYouYet": "Ainda nada partilhado consigo", + "noAlbumsSharedByYouYet": "Ainda não há álbuns partilhados por si", + "sharedWithYou": "Partilhado consigo", + "sharedByYou": "Partilhado por si", + "inviteYourFriendsToEnte": "Convide seus amigos para o Ente", + "failedToDownloadVideo": "Falha ao fazer o download do vídeo", "hiding": "Ocultando...", "unhiding": "Reexibindo...", "successfullyHid": "Ocultado com sucesso", - "successfullyUnhid": "Desocultado com sucesso", - "crashReporting": "Relatório de erros", - "resumableUploads": "Envios retomáveis", - "addToHiddenAlbum": "Adicionar ao álbum oculto", - "moveToHiddenAlbum": "Mover ao álbum oculto", + "successfullyUnhid": "Reexibido com sucesso", + "crashReporting": "Relatório de falhas", + "resumableUploads": "Uploads reenviados", + "addToHiddenAlbum": "Adicionar a álbum oculto", + "moveToHiddenAlbum": "Mover para álbum oculto", "fileTypes": "Tipos de arquivo", - "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", + "deleteConfirmDialogBody": "Esta conta está ligada a outras aplicações Ente, se utilizar alguma. Os seus dados carregados, em todas as aplicações Ente, serão agendados para eliminação e a sua conta será permanentemente eliminada.", + "hearUsWhereTitle": "Como é que soube do Ente? (opcional)", + "hearUsExplanation": "Não monitorizamos as instalações de aplicações. Ajudaria se nos dissesse onde nos encontrou!", + "viewAddOnButton": "Ver addons", + "addOns": "addons", + "addOnPageSubtitle": "Detalhes dos addons", "yourMap": "Seu mapa", - "modifyYourQueryOrTrySearchingFor": "Altere o termo de busca ou tente consultar", + "modifyYourQueryOrTrySearchingFor": "Modifique a sua consulta ou tente pesquisar por", "blackFridaySale": "Promoção Black Friday", - "upto50OffUntil4thDec": "Com 50% de desconto, até 4 de dezembro", + "upto50OffUntil4thDec": "Até 50% de desconto, até 4 de dezembro.", "photos": "Fotos", "videos": "Vídeos", - "livePhotos": "Fotos animadas", - "searchHint1": "busca rápida no dispositivo", - "searchHint2": "Descrições e data das fotos", + "livePhotos": "Fotos Em Tempo Real", + "searchHint1": "Pesquisa rápida no dispositivo", + "searchHint2": "Datas das fotos, descrições", "searchHint3": "Álbuns, nomes de arquivos e tipos", - "searchHint4": "Localização", - "searchHint5": "Em breve: Busca mágica e rostos ✨", + "searchHint4": "Local", + "searchHint5": "Em breve: Rostos e pesquisa mágica ✨", "addYourPhotosNow": "Adicione suas fotos agora", - "searchResultCount": "{count, plural, one{{count} resultado encontrado} other{{count} resultados encontrados}}", + "searchResultCount": "{count, plural, one{{count} ano atrás} other{{count} anos atrás}}", "@searchResultCount": { "description": "Text to tell user how many results were found for their search query", "placeholders": { @@ -1232,88 +1186,86 @@ "@addNew": { "description": "Text to add a new item (location tag, album, caption etc)" }, - "contacts": "Contatos", - "noInternetConnection": "Sem conexão à internet", - "pleaseCheckYourInternetConnectionAndTryAgain": "Verifique sua conexão com a internet e tente novamente.", - "signOutFromOtherDevices": "Sair da conta em outros dispositivos", - "signOutOtherBody": "Se você acha que alguém possa saber da sua senha, você pode forçar desconectar sua conta de outros dispositivos.", - "signOutOtherDevices": "Sair em outros dispositivos", - "doNotSignOut": "Não sair", + "contacts": "Contactos", + "noInternetConnection": "Sem ligação à internet", + "pleaseCheckYourInternetConnectionAndTryAgain": "Por favor, verifique a sua ligação à Internet e tente novamente.", + "signOutFromOtherDevices": "Terminar sessão noutros dispositivos", + "signOutOtherBody": "Se pensa que alguém pode saber a sua palavra-passe, pode forçar todos os outros dispositivos que utilizam a sua conta a terminar a sessão.", + "signOutOtherDevices": "Terminar a sessão noutros dispositivos", + "doNotSignOut": "Não terminar a sessão", "editLocation": "Editar localização", - "selectALocation": "Selecionar localização", - "selectALocationFirst": "Primeiramente selecione uma localização", + "selectALocation": "Selecione uma localização", + "selectALocationFirst": "Selecione uma localização primeiro", "changeLocationOfSelectedItems": "Alterar a localização dos itens selecionados?", - "editsToLocationWillOnlyBeSeenWithinEnte": "Edições à localização serão apenas vistos no Ente", - "cleanUncategorized": "Limpar não categorizado", - "cleanUncategorizedDescription": "Remover todos os arquivos não categorizados que estão presentes em outros álbuns", - "waitingForVerification": "Esperando verificação...", + "editsToLocationWillOnlyBeSeenWithinEnte": "Edições para localização só serão vistas dentro do Ente", + "cleanUncategorized": "Limpar sem categoria", + "cleanUncategorizedDescription": "Remover todos os arquivos da Não Categorizados que estão presentes em outros álbuns", + "waitingForVerification": "Aguardando verificação...", "passkey": "Chave de acesso", - "passkeyAuthTitle": "Verificação de chave de acesso", - "loginWithTOTP": "Registrar com TOTP", - "passKeyPendingVerification": "Verificação pendente", + "passkeyAuthTitle": "Verificação da chave de acesso", + "loginWithTOTP": "Iniciar sessão com TOTP", + "passKeyPendingVerification": "A verificação ainda está pendente", "loginSessionExpired": "Sessão expirada", - "loginSessionExpiredDetails": "Sua sessão expirou. Registre-se novamente.", + "loginSessionExpiredDetails": "A sua sessão expirou. Por favor, inicie sessão novamente.", "verifyPasskey": "Verificar chave de acesso", "playOnTv": "Reproduzir álbum na TV", - "pair": "Parear", + "pair": "Emparelhar", "deviceNotFound": "Dispositivo não encontrado", - "castInstruction": "Acesse cast.ente.io no dispositivo desejado para parear.\n\nInsira o código abaixo para reproduzir o álbum na sua TV.", - "deviceCodeHint": "Insira o código", - "joinDiscord": "Junte-se ao Discord", + "castInstruction": "Visite cast.ente.io no dispositivo que pretende emparelhar.\n\n\nIntroduza o código abaixo para reproduzir o álbum na sua TV.", + "deviceCodeHint": "Introduza o código", + "joinDiscord": "Juntar-se ao Discord", "locations": "Localizações", - "addAName": "Adicione um nome", - "findThemQuickly": "Busque-os rapidamente", + "addAName": "Adiciona um nome", + "findThemQuickly": "Ache-os rapidamente", "@findThemQuickly": { "description": "Subtitle to indicate that the user can find people quickly by name" }, - "findPeopleByName": "Busque pessoas facilmente pelo nome", - "addViewers": "{count, plural, one {Adicionar visualizador} one {Adicionar visualizador} other {Adicionar visualizadores}}", - "addCollaborators": "{count, plural, one {Adicionar colaborador} one {Adicionar colaborador} other {Adicionar colaboradores}}", - "longPressAnEmailToVerifyEndToEndEncryption": "Pressione um e-mail para verificar a criptografia ponta a ponta.", - "developerSettingsWarning": "Deseja modificar as Opções de Desenvolvedor?", - "developerSettings": "Opções de desenvolvedor", - "serverEndpoint": "Ponto final do servidor", - "invalidEndpoint": "Ponto final inválido", - "invalidEndpointMessage": "Desculpe, o ponto final inserido é inválido. Insira um ponto final válido e tente novamente.", - "endpointUpdatedMessage": "Ponto final atualizado com sucesso", - "customEndpoint": "Conectado à {endpoint}", + "findPeopleByName": "Encontrar pessoas rapidamente pelo nome", + "longPressAnEmailToVerifyEndToEndEncryption": "Pressione e segure um e-mail para verificar a criptografia de ponta a ponta.", + "developerSettingsWarning": "Tem a certeza de que pretende modificar as definições de programador?", + "developerSettings": "Definições do programador", + "serverEndpoint": "Endpoint do servidor", + "invalidEndpoint": "Endpoint inválido", + "invalidEndpointMessage": "Desculpe, o endpoint que introduziu é inválido. Introduza um ponto final válido e tente novamente.", + "endpointUpdatedMessage": "Endpoint atualizado com sucesso", + "customEndpoint": "Conectado a {endpoint}", "createCollaborativeLink": "Criar link colaborativo", - "search": "Buscar", + "search": "Pesquisar", "enterPersonName": "Inserir nome da pessoa", "enterName": "Inserir nome", - "savePerson": "Salvar pessoa", + "savePerson": "Guardar pessoa", "editPerson": "Editar pessoa", - "mergedPhotos": "Fotos mescladas", - "orMergeWithExistingPerson": "Ou mesclar com existente", + "mergedPhotos": "Fotos combinadas", + "orMergeWithExistingPerson": "Ou combinar com já existente", "enterDateOfBirth": "Aniversário (opcional)", "birthday": "Aniversário", "removePersonLabel": "Remover etiqueta da pessoa", - "autoPairDesc": "O pareamento automático só funciona com dispositivos que suportam o Chromecast.", - "manualPairDesc": "Parear com PIN funciona com qualquer tela que queira visualizar seu álbum.", - "connectToDevice": "Conectar ao dispositivo", - "autoCastDialogBody": "Você verá dispositivos de transmissão disponível aqui.", - "autoCastiOSPermission": "Certifique-se que as permissões da internet local estejam ligadas para o Ente Photos App, em opções.", + "autoPairDesc": "O pareamento automático funciona apenas com dispositivos que suportam o Chromecast.", + "manualPairDesc": "Emparelhar com PIN funciona com qualquer ecrã onde pretenda ver o seu álbum.", + "connectToDevice": "Ligar ao dispositivo", + "autoCastDialogBody": "Verá os dispositivos Cast disponíveis aqui.", + "autoCastiOSPermission": "Certifique-se de que as permissões de Rede local estão activadas para a aplicação Ente Photos, nas Definições.", "noDeviceFound": "Nenhum dispositivo encontrado", "stopCastingTitle": "Parar transmissão", - "stopCastingBody": "Deseja parar a transmissão?", - "castIPMismatchTitle": "Falhou ao transmitir álbum", - "castIPMismatchBody": "Certifique-se de estar na mesma internet que a TV.", - "pairingComplete": "Pareamento concluído", - "savingEdits": "Salvando edições...", - "autoPair": "Pareamento automático", - "pairWithPin": "Parear com PIN", + "stopCastingBody": "Queres parar de fazer transmissão?", + "castIPMismatchTitle": "Falha ao transmitir álbum", + "castIPMismatchBody": "Certifique-se de estar na mesma rede que a TV.", + "pairingComplete": "Emparelhamento concluído", + "savingEdits": "Gravando edições...", + "autoPair": "Emparelhamento automático", + "pairWithPin": "Emparelhar com PIN", "faceRecognition": "Reconhecimento facial", "foundFaces": "Rostos encontrados", "clusteringProgress": "Progresso de agrupamento", - "indexingIsPaused": "A indexação parou, ela será retomada automaticamente quando o dispositivo estiver pronto.", - "trim": "Recortar", - "crop": "Cortar", - "rotate": "Girar", + "indexingIsPaused": "A indexação está pausada, será retomada automaticamente quando o dispositivo estiver pronto.", + "trim": "Cortar", + "crop": "Recortar", + "rotate": "Rodar", "left": "Esquerda", "right": "Direita", "whatsNew": "O que há de novo", "reviewSuggestions": "Revisar sugestões", - "review": "Revisar", + "review": "Rever", "useAsCover": "Usar como capa", "notPersonLabel": "Não é {name}?", "@notPersonLabel": { @@ -1328,49 +1280,49 @@ "enable": "Ativar", "enabled": "Ativado", "moreDetails": "Mais detalhes", - "enableMLIndexingDesc": "Ente suporta aprendizagem de máquina para reconhecimento facial, busca mágica e outros recursos de busca avançados", - "magicSearchHint": "A busca mágica permite buscar fotos pelo conteúdo, p. e.x. 'flor', 'carro vermelho', 'identidade'", + "enableMLIndexingDesc": "O Ente suporta a aprendizagem automática no dispositivo para reconhecimento facial, pesquisa mágica e outras funcionalidades de pesquisa avançadas", + "magicSearchHint": "A pesquisa mágica permite pesquisar fotos por seu conteúdo, por exemplo, 'flor', 'carro vermelho', 'documentos de identidade'", "panorama": "Panorama", - "reenterPassword": "Reinserir senha", - "reenterPin": "Reinserir PIN", + "reenterPassword": "Insira novamente a palavra-passe", + "reenterPin": "Inserir PIN novamente", "deviceLock": "Bloqueio do dispositivo", "pinLock": "Bloqueio por PIN", - "next": "Próximo", - "setNewPassword": "Definir nova senha", - "enterPin": "Inserir PIN", - "setNewPin": "Definir PIN novo", - "appLock": "Bloqueio do aplicativo", - "noSystemLockFound": "Nenhum bloqueio do sistema encontrado", + "next": "Seguinte", + "setNewPassword": "Definir nova palavra-passe", + "enterPin": "Introduzir PIN", + "setNewPin": "Definir novo PIN", + "appLock": "Bloqueio de app", + "noSystemLockFound": "Nenhum bloqueio de sistema encontrado", "tapToUnlock": "Toque para desbloquear", "tooManyIncorrectAttempts": "Muitas tentativas incorretas", - "videoInfo": "Informações do vídeo", + "videoInfo": "Informação de Vídeo", "autoLock": "Bloqueio automático", "immediately": "Imediatamente", - "autoLockFeatureDescription": "Tempo após o qual o aplicativo bloqueia após ser colocado em segundo plano", + "autoLockFeatureDescription": "Tempo após o qual a aplicação bloqueia depois de ser colocada em segundo plano", "hideContent": "Ocultar conteúdo", - "hideContentDescriptionAndroid": "Oculta os conteúdos do aplicativo no seletor de aplicativos e desativa capturas de tela", - "hideContentDescriptionIos": "Oculta o conteúdo no seletor de aplicativos", - "passwordStrengthInfo": "A força da senha é calculada considerando o comprimento dos dígitos, carácteres usados, e se ou não a senha aparece nas 10.000 senhas usadas.", + "hideContentDescriptionAndroid": "Oculta o conteúdo da aplicação no alternador de aplicações e desactiva as capturas de ecrã", + "hideContentDescriptionIos": "Oculta o conteúdo da aplicação no alternador de aplicações", + "passwordStrengthInfo": "A força da palavra-passe é calculada tendo em conta o comprimento da palavra-passe, os caracteres utilizados e se a palavra-passe aparece ou não nas 10.000 palavras-passe mais utilizadas", "noQuickLinksSelected": "Nenhum link rápido selecionado", "pleaseSelectQuickLinksToRemove": "Selecione links rápidos para remover", "removePublicLinks": "Remover link público", "thisWillRemovePublicLinksOfAllSelectedQuickLinks": "Isto removerá links públicos de todos os links rápidos selecionados.", - "guestView": "Vista do convidado", - "guestViewEnablePreSteps": "Para ativar a vista do convidado, defina uma senha de acesso no dispositivo ou bloqueie sua tela nas opções do sistema.", - "nameTheAlbum": "Nomear álbum", + "guestView": "Visão de convidado", + "guestViewEnablePreSteps": "Para ativar a vista de convidado, configure o código de acesso do dispositivo ou o bloqueio do ecrã nas definições do sistema.", + "nameTheAlbum": "Nomear o álbum", "collectPhotosDescription": "Crie um link onde seus amigos podem enviar fotos na qualidade original.", - "collect": "Coletar", - "appLockDescriptions": "Escolha entre a tela de bloqueio padrão do seu dispositivo e uma tela de bloqueio personalizada com PIN ou senha.", - "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "Para ativar o bloqueio do aplicativo, defina uma senha de acesso no dispositivo ou bloqueie sua tela nas opções do sistema.", - "authToViewPasskey": "Autentique-se para ver sua chave de acesso", - "loopVideoOn": "Repetir vídeo ativado", - "loopVideoOff": "Repetir vídeo desativado", - "localSyncErrorMessage": "Ocorreu um erro devido à sincronização de localização das fotos estar levando mais tempo que o esperado. Entre em contato conosco.", + "collect": "Recolher", + "appLockDescriptions": "Escolha entre o ecrã de bloqueio predefinido do seu dispositivo e um ecrã de bloqueio personalizado com um PIN ou uma palavra-passe.", + "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "Para ativar o bloqueio de aplicações, configure o código de acesso do dispositivo ou o bloqueio de ecrã nas definições do sistema.", + "authToViewPasskey": "Autentique-se para ver a sua chave de acesso", + "loopVideoOn": "Repetir vídeo ligado", + "loopVideoOff": "Repetir vídeo desligado", + "localSyncErrorMessage": "Parece que algo correu mal, uma vez que a sincronização de fotografias locais está a demorar mais tempo do que o esperado. Contacte a nossa equipa de apoio", "showPerson": "Mostrar pessoa", "sort": "Ordenar", "mostRecent": "Mais recente", "mostRelevant": "Mais relevante", - "loadingYourPhotos": "Carregando suas fotos...", + "loadingYourPhotos": "Carregar as suas fotos...", "processingImport": "Processando {folderName}...", "personName": "Nome da pessoa", "addNewPerson": "Adicionar nova pessoa", @@ -1379,7 +1331,7 @@ "newPerson": "Nova pessoa", "addName": "Adicionar pessoa", "add": "Adicionar", - "extraPhotosFoundFor": "Fotos adicionais encontradas para {text}", + "extraPhotosFoundFor": "Fotos extras encontradas para {text}", "@extraPhotosFoundFor": { "placeholders": { "text": { @@ -1390,26 +1342,13 @@ "extraPhotosFound": "Fotos adicionais encontradas", "configuration": "Configuração", "localIndexing": "Indexação local", - "processed": "Processado", "resetPerson": "Remover", - "areYouSureYouWantToResetThisPerson": "Deseja redefinir esta pessoa?", - "allPersonGroupingWillReset": "Todos os agrupamentos dessa pessoa serão redefinidos, e você perderá todas as sugestões feitas por essa pessoa.", - "yesResetPerson": "Sim, redefinir pessoa", + "areYouSureYouWantToResetThisPerson": "Tens a certeza de que queres repor esta pessoa?", + "allPersonGroupingWillReset": "Todos os agrupamentos para esta pessoa serão reiniciados e perderá todas as sugestões feitas para esta pessoa", + "yesResetPerson": "Sim, repor pessoa", "onlyThem": "Apenas eles", - "checkingModels": "Verificando modelos...", - "enableMachineLearningBanner": "Ativar aprendizagem de máquina para busca mágica e reconhecimento facial", - "searchDiscoverEmptySection": "As imagens serão exibidas aqui quando o processamento e sincronização for concluído", - "searchPersonsEmptySection": "As pessoas serão exibidas aqui quando o processamento e sincronização for concluído", - "viewersSuccessfullyAdded": "{count, plural, =0 {Adicionado 0 visualizadores} =1 {Adicionado 1 visualizador} other {Adicionado {count} visualizadores}}", - "@viewersSuccessfullyAdded": { - "placeholders": { - "count": { - "type": "int", - "example": "2" - } - }, - "description": "Number of viewers that were successfully added to an album." - }, + "checkingModels": "A verificar modelos...", + "enableMachineLearningBanner": "Habilitar aprendizagem automática para pesquisa mágica e reconhecimento de rosto", "collaboratorsSuccessfullyAdded": "{count, plural, =0 {Adicionado 0 colaboradores} =1 {Adicionado 1 colaborador} other {Adicionado {count} colaboradores}}", "@collaboratorsSuccessfullyAdded": { "placeholders": { @@ -1420,18 +1359,18 @@ }, "description": "Number of collaborators that were successfully added to an album." }, - "accountIsAlreadyConfigured": "A conta já está configurada.", + "accountIsAlreadyConfigured": "A conta já está ajustada.", "sessionIdMismatch": "Incompatibilidade de ID de sessão", "@sessionIdMismatch": { "description": "In passkey page, deeplink is ignored because of session ID mismatch." }, - "failedToFetchActiveSessions": "Falhou ao obter sessões ativas", + "failedToFetchActiveSessions": "Falha ao obter sessões em atividade", "@failedToFetchActiveSessions": { "description": "In session page, warn user (in toast) that active sessions could not be fetched." }, - "failedToRefreshStripeSubscription": "Falhou ao atualizar assinatura", - "failedToPlayVideo": "Falhou ao reproduzir vídeo", - "uploadIsIgnoredDueToIgnorereason": "O envio é ignorado devido a {ignoreReason}", + "failedToRefreshStripeSubscription": "Falha ao atualizar subscrição", + "failedToPlayVideo": "Falha ao reproduzir multimédia", + "uploadIsIgnoredDueToIgnorereason": "Envio ignorado devido à {ignoreReason}", "@uploadIsIgnoredDueToIgnorereason": { "placeholders": { "ignoreReason": { @@ -1440,7 +1379,7 @@ } } }, - "typeOfGallerGallerytypeIsNotSupportedForRename": "O tipo de galeria {galleryType} não é suportado para renomear", + "typeOfGallerGallerytypeIsNotSupportedForRename": "Tipo de galeria {galleryType} não é permitido para renomear", "@typeOfGallerGallerytypeIsNotSupportedForRename": { "placeholders": { "galleryType": { @@ -1736,5 +1675,11 @@ "cLFamilyPlanDesc": "Agora você pode definir um limite de quanto armazenamento os seus entes queridos podem usar.", "cLBulkEdit": "Editar todas as datas", "cLBulkEditDesc": "Agora você pode selecionar várias fotos, editar data e hora de todos com um só clique. Alternar datas também são suportados.", - "curatedMemories": "Curated memories" + "curatedMemories": "Curated memories", + "onThisDay": "On this day", + "lookBackOnYourMemories": "Look back on your memories 🌄", + "newPhotosEmoji": " new 📸", + "sorryWeHadToPauseYourBackups": "Sorry, we had to pause your backups", + "clickToInstallOurBestVersionYet": "Click to install our best version yet", + "onThisDayNotificationExplanation": "Receive reminders about memories from this day in previous years." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_pt_BR.arb b/mobile/lib/l10n/intl_pt_BR.arb index 7cc841ec62..a8754df05f 100644 --- a/mobile/lib/l10n/intl_pt_BR.arb +++ b/mobile/lib/l10n/intl_pt_BR.arb @@ -1,6 +1,7 @@ { "@@locale ": "en", "enterYourEmailAddress": "Insira seu endereço de e-mail", + "enterYourNewEmailAddress": "Insira seu novo e-mail", "accountWelcomeBack": "Bem-vindo(a) de volta!", "emailAlreadyRegistered": "E-mail já registrado.", "emailNotRegistered": "E-mail não registrado.", @@ -1281,6 +1282,8 @@ "createCollaborativeLink": "Criar link colaborativo", "search": "Buscar", "enterPersonName": "Inserir nome da pessoa", + "editEmailAlreadyLinked": "Este e-mail já está vinculado a {name}.", + "viewPersonToUnlink": "Visualizar {name} para desvincular", "enterName": "Inserir nome", "savePerson": "Salvar pessoa", "editPerson": "Editar pessoa", @@ -1660,7 +1663,7 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Transmissão de vídeo", + "videoStreaming": "Vídeos transmissíveis", "processingVideos": "Processando vídeos", "streamDetails": "Detalhes da transmissão", "processing": "Processando", @@ -1737,5 +1740,10 @@ "cLFamilyPlanDesc": "Agora você pode definir um limite de quanto armazenamento os seus entes queridos podem usar.", "cLBulkEdit": "Editar todas as datas", "cLBulkEditDesc": "Agora você pode selecionar várias fotos, editar data e hora de todos com um só clique. Alternar datas também são suportados.", - "curatedMemories": "Memórias restauradas" + "curatedMemories": "Memórias restauradas", + "onThisDay": "Neste dia", + "deleteMultipleAlbumDialog": "E também excluir todas as fotos (e vídeos) presente dentro desses {count} álbuns e de todos os álbuns que eles fazem parte?", + "addParticipants": "Adicionar participante", + "selectedAlbums": "{count} selecionado(s)", + "actionNotSupportedOnFavouritesAlbum": "Ação não suportada em álbum favorito" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_pt_PT.arb b/mobile/lib/l10n/intl_pt_PT.arb index 95b22dab3f..b1bd6e5ac2 100644 --- a/mobile/lib/l10n/intl_pt_PT.arb +++ b/mobile/lib/l10n/intl_pt_PT.arb @@ -237,7 +237,7 @@ "publicLinkEnabled": "Link público ativado", "shareALink": "Partilhar um link", "sharedAlbumSectionDescription": "Criar álbuns compartilhados e colaborativos com outros usuários da Ente, incluindo usuários em planos gratuitos.", - "shareWithPeopleSectionTitle": "{numberOfPeople, plural, one {}=0 {Compartilhe com pessoas específicas} =1 {Compartilhado com 1 pessoa} other {Compartilhado com {numberOfPeople} pessoas}}", + "shareWithPeopleSectionTitle": "{numberOfPeople, plural, =0 {Compartilhe com pessoas específicas} =1 {Compartilhado com 1 pessoa} other {Compartilhado com {numberOfPeople} pessoas}}", "@shareWithPeopleSectionTitle": { "placeholders": { "numberOfPeople": { diff --git a/mobile/lib/l10n/intl_ru.arb b/mobile/lib/l10n/intl_ru.arb index f72d953a43..b58ff60291 100644 --- a/mobile/lib/l10n/intl_ru.arb +++ b/mobile/lib/l10n/intl_ru.arb @@ -220,7 +220,7 @@ "after1Month": "Через 1 месяц", "after1Year": "Через 1 год", "manageParticipants": "Управлять", - "albumParticipantsCount": "{count, plural, =0 {Нет участников} =1 {{count} участник} one {{count} участник} few {{count} участника} other {{count} участников}}", + "albumParticipantsCount": "{count, plural, =0 {Нет участников} =1 {{count} участник} other {{count} участников}}", "@albumParticipantsCount": { "placeholders": { "count": { @@ -371,7 +371,7 @@ "deleteFromBoth": "Удалить из обоих мест", "newAlbum": "Новый альбом", "albums": "Альбомы", - "memoryCount": "{count, plural, =0{нет воспоминаний} one{{formattedCount} воспоминание} few{{formattedCount} воспоминания} other{{formattedCount} воспоминаний}}", + "memoryCount": "{count, plural, =0{нет воспоминаний} one{{formattedCount} воспоминание} other{{formattedCount} воспоминаний}}", "@memoryCount": { "description": "The text to display the number of memories", "type": "text", @@ -458,8 +458,8 @@ "selectAll": "Выбрать все", "skip": "Пропустить", "updatingFolderSelection": "Обновление выбора папок...", - "itemCount": "{count, plural, one{{count} элемент} few{{count} элемента} other{{count} элементов}}", - "deleteItemCount": "{count, plural, =1 {Удалить {count} элемент} one {Удалить {count} элемент} few {Удалить {count} элемента} other {Удалить {count} элементов}}", + "itemCount": "{count, plural, one{{count} элемент} other{{count} элементов}}", + "deleteItemCount": "{count, plural, =1 {Удалить {count} элемент} other {Удалить {count} элементов}}", "duplicateItemsGroup": "{count} файлов, по {formattedSize} каждый", "@duplicateItemsGroup": { "description": "Display the number of duplicate files and their size", @@ -542,7 +542,7 @@ }, "remindToEmptyEnteTrash": "Также очистите «Корзину», чтобы освободить место", "sparkleSuccess": "✨ Успех", - "duplicateFileCountWithStorageSaved": "Вы удалили {count, plural, one{{count} дубликат} few{{count} дубликата} other{{count} дубликатов}}, освободив ({storageSaved}!)", + "duplicateFileCountWithStorageSaved": "Вы удалили {count, plural, one{{count} дубликат} other{{count} дубликатов}}, освободив ({storageSaved}!)", "@duplicateFileCountWithStorageSaved": { "description": "The text to display when the user has successfully cleaned up duplicate files", "type": "text", @@ -824,7 +824,7 @@ "referFriendsAnd2xYourPlan": "Пригласите друзей и удвойте свой тариф", "shareAlbumHint": "Откройте альбом и нажмите кнопку «Поделиться» в правом верхнем углу, чтобы поделиться.", "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": "На элементах отображается количество дней, оставшихся до их безвозвратного удаления", - "trashDaysLeft": "{count, plural, =0 {Скоро} =1 {1 день} one{{count} день} few{{count} дня} other {{count} дней}}", + "trashDaysLeft": "{count, plural, =0 {Скоро} =1 {1 день} other {{count} дней}}", "@trashDaysLeft": { "description": "Text to indicate number of days remaining before permanent deletion", "placeholders": { @@ -898,7 +898,7 @@ "unlock": "Разблокировать", "freeUpSpace": "Освободить место", "freeUpSpaceSaving": "{count, plural, =1 {Его можно удалить с устройства, чтобы освободить {formattedSize}} other {Их можно удалить с устройства, чтобы освободить {formattedSize}}}", - "filesBackedUpInAlbum": "{count, plural, one {{formattedNumber} файл в этом альбоме был успешно сохранён} few{{formattedNumber} файла в этом альбоме были успешно сохранены} other {{formattedNumber} файлов в этом альбоме были успешно сохранены}}", + "filesBackedUpInAlbum": "{count, plural, one {{formattedNumber} файл в этом альбоме был успешно сохранён} other {{formattedNumber} файлов в этом альбоме были успешно сохранены}}", "@filesBackedUpInAlbum": { "description": "Text to tell user how many files have been backed up in the album", "placeholders": { @@ -913,7 +913,7 @@ } } }, - "filesBackedUpFromDevice": "{count, plural, one {{formattedNumber} файл на этом устройстве был успешно сохранён} few{{formattedNumber} файла на этом устройстве были успешно сохранены} other {{formattedNumber} файлов на этом устройстве были успешно сохранены}}", + "filesBackedUpFromDevice": "{count, plural, one {{formattedNumber} файл на этом устройстве был успешно сохранён} other {{formattedNumber} файлов на этом устройстве были успешно сохранены}}", "@filesBackedUpFromDevice": { "description": "Text to tell user how many files have been backed up from this device", "placeholders": { @@ -1214,7 +1214,7 @@ "searchHint4": "Местоположение", "searchHint5": "Скоро: Лица и магический поиск ✨", "addYourPhotosNow": "Добавьте ваши фото", - "searchResultCount": "{count, plural, one{{count} результат найден} few{{count} результата найдено} other {{count} результатов найдено}}", + "searchResultCount": "{count, plural, one{{count} результат найден} other {{count} результатов найдено}}", "@searchResultCount": { "description": "Text to tell user how many results were found for their search query", "placeholders": { @@ -1409,7 +1409,7 @@ }, "description": "Number of viewers that were successfully added to an album." }, - "collaboratorsSuccessfullyAdded": "{count, plural, =0 {Добавлено 0 соавторов} =1 {Добавлен 1 соавтор} one{Добавлен {count} соавтор} few{Добавлено {count} соавтора} other {Добавлено {count} соавторов}}", + "collaboratorsSuccessfullyAdded": "{count, plural, =0 {Добавлено 0 соавторов} =1 {Добавлен 1 соавтор} other {Добавлено {count} соавторов}}", "@collaboratorsSuccessfullyAdded": { "placeholders": { "count": { @@ -1484,7 +1484,7 @@ }, "currentlyRunning": "выполняется", "ignored": "игнорируется", - "photosCount": "{count, plural, =0 {0 фотографий} =1 {1 фотография} one {{count} фотография} few {{count} фотографии} other {{count} фотографий}}", + "photosCount": "{count, plural, =0 {0 фотографий} =1 {1 фотография} other {{count} фотографий}}", "@photosCount": { "placeholders": { "count": { @@ -1658,7 +1658,6 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Потоковое видео", "processingVideos": "Обработка видео", "streamDetails": "Информация о потоке", "processing": "Обработка", @@ -1696,7 +1695,7 @@ "selectedItemsWillBeRemovedFromThisPerson": "Выбранные элементы будут отвязаны от этого человека, но не удалены из вашей библиотеки.", "throughTheYears": "{dateFormat} сквозь годы", "thisWeekThroughTheYears": "Эта неделя сквозь годы", - "thisWeekXYearsAgo": "{count, plural, =1 {Эта неделя, {count} год назад} one{Эта неделя, {count} год назад} few{Эта неделя, {count} года назад} other {Эта неделя, {count} лет назад}}", + "thisWeekXYearsAgo": "{count, plural, =1 {Эта неделя, {count} год назад} other {Эта неделя, {count} лет назад}}", "youAndThem": "Вы и {name}", "admiringThem": "Любуясь {name}", "embracingThem": "Обнимая {name}", diff --git a/mobile/lib/l10n/intl_ta.arb b/mobile/lib/l10n/intl_ta.arb index d3d26e203c..1b9d1e8190 100644 --- a/mobile/lib/l10n/intl_ta.arb +++ b/mobile/lib/l10n/intl_ta.arb @@ -2,6 +2,8 @@ "@@locale ": "en", "enterYourEmailAddress": "உங்கள் மின்னஞ்சல் முகவரியை உள்ளிடவும்", "accountWelcomeBack": "மீண்டும் வருக!", + "emailAlreadyRegistered": "மின்னஞ்சல் முன்பே பதிவுசெய்யப்பட்டுள்ளது.", + "emailNotRegistered": "மின்னஞ்சல் பதிவு செய்யப்படவில்லை.", "email": "மின்னஞ்சல்", "cancel": "ரத்து செய்", "verify": "சரிபார்க்கவும்", @@ -15,5 +17,7 @@ "confirmDeletePrompt": "ஆம், எல்லா செயலிகளிலும் இந்தக் கணக்கையும் அதன் தரவையும் நிரந்தரமாக நீக்க விரும்புகிறேன்.", "confirmAccountDeletion": "கணக்கு நீக்குதலை உறுதிப்படுத்தவும்", "deleteAccountPermanentlyButton": "கணக்கை நிரந்தரமாக நீக்கவும்", + "yourAccountHasBeenDeleted": "உங்கள் கணக்கு நீக்கப்பட்டது", + "selectReason": "காரணத்தைத் தேர்ந்தெடுக்கவும்", "deleteReason1": "எனக்கு தேவையான ஒரு முக்கிய அம்சம் இதில் இல்லை" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_tr.arb b/mobile/lib/l10n/intl_tr.arb index c3d8aa5547..c3ae396543 100644 --- a/mobile/lib/l10n/intl_tr.arb +++ b/mobile/lib/l10n/intl_tr.arb @@ -185,19 +185,19 @@ "@allowAddingPhotos": { "description": "Switch button to enable uploading photos to a public link" }, - "allowAddPhotosDescription": "Bağlantıya sahip olan kişilere, paylaşılan albüme fotoğraf eklemelerine izin ver.", + "allowAddPhotosDescription": "Bağlantıya sahip olan kişilerin paylaşılan albüme fotoğraf eklemelerine izin ver.", "passwordLock": "Şifre kilidi", "canNotOpenTitle": "Albüm açılamadı", "canNotOpenBody": "Üzgünüz, Bu albüm uygulama içinde açılamadı.", "disableDownloadWarningTitle": "Lütfen dikkate alın", "disableDownloadWarningBody": "Görüntüleyiciler, hala harici araçlar kullanarak ekran görüntüsü alabilir veya fotoğraflarınızın bir kopyasını kaydedebilir. Lütfen bunu göz önünde bulundurunuz", "allowDownloads": "İndirmeye izin ver", - "linkDeviceLimit": "Cihaz limiti", + "linkDeviceLimit": "Cihaz sınırı", "noDeviceLimit": "Yok", "@noDeviceLimit": { "description": "Text to indicate that there is limit on number of devices" }, - "linkExpiry": "Linkin geçerliliği", + "linkExpiry": "Bağlantı geçerliliği", "linkExpired": "Süresi dolmuş", "linkEnabled": "Geçerli", "linkNeverExpires": "Asla", @@ -205,12 +205,12 @@ "setAPassword": "Şifre ayarla", "lockButtonLabel": "Kilit", "enterPassword": "Şifrenizi girin", - "removeLink": "Linki kaldır", - "manageLink": "Linki yönet", - "linkExpiresOn": "Bu bağlantı {expiryTime} dan sonra geçersiz olacaktır", + "removeLink": "Bağlantıyı kaldır", + "manageLink": "Bağlantıyı yönet", + "linkExpiresOn": "Bu bağlantı {expiryTime} tarihinden itibaren geçersiz olacaktır", "albumUpdated": "Albüm güncellendi", "never": "Asla", - "custom": "Kişisel", + "custom": "Özel", "@custom": { "description": "Label for setting custom value for link expiry" }, @@ -232,14 +232,14 @@ }, "collabLinkSectionDescription": "Ente aplikasyonu veya hesabı olmadan insanların paylaşılan albümde fotoğraf ekleyip görüntülemelerine izin vermek için bir bağlantı oluşturun. Grup veya etkinlik fotoğraflarını toplamak için harika bir seçenek.", "collectPhotos": "Fotoğrafları topla", - "collaborativeLink": "Organizasyon bağlantısı", + "collaborativeLink": "Ortak bağlantı", "shareWithNonenteUsers": "Ente kullanıcısı olmayanlar için paylaş", - "createPublicLink": "Herkese açık link oluştur", - "sendLink": "Link gönder", - "copyLink": "Linki kopyala", + "createPublicLink": "Herkese açık bir bağlantı oluştur", + "sendLink": "Bağlantıyı gönder", + "copyLink": "Bağlantıyı kopyala", "linkHasExpired": "Bağlantının süresi dolmuş", "publicLinkEnabled": "Herkese açık bağlantı aktive edildi", - "shareALink": "Linki paylaş", + "shareALink": "Bir bağlantı paylaş", "sharedAlbumSectionDescription": "Diğer Ente kullanıcılarıyla paylaşılan ve topluluk albümleri oluşturun, bu arada ücretsiz planlara sahip kullanıcıları da içerir.", "shareWithPeopleSectionTitle": "{numberOfPeople, plural, =0 {Belirli kişilerle paylaş} =1 {1 kişiyle paylaşıldı} other {{numberOfPeople} kişiyle paylaşıldı}}", "@shareWithPeopleSectionTitle": { @@ -345,7 +345,7 @@ "keepPhotos": "Fotoğrafları sakla", "deletePhotos": "Fotoğrafları sil", "inviteToEnte": "Ente'ye davet edin", - "removePublicLink": "Herkese açık link oluştur", + "removePublicLink": "Herkese açık bağlantıyı kaldır", "disableLinkMessage": "Bu, \"{albumName}\"e erişim için olan genel bağlantıyı kaldıracaktır.", "sharing": "Paylaşılıyor...", "youCannotShareWithYourself": "Kendinizle paylaşamazsınız", @@ -738,7 +738,7 @@ "description": "The text displayed above folders/albums stored on device", "type": "text" }, - "onDevice": "Bu cihaz", + "onDevice": "Cihazda", "@onEnte": { "description": "The text displayed above albums backed up to Ente", "type": "text" @@ -768,10 +768,10 @@ }, "permanentlyDelete": "Kalıcı olarak sil", "canOnlyCreateLinkForFilesOwnedByYou": "Yalnızca size ait dosyalar için bağlantı oluşturabilir", - "publicLinkCreated": "Herkese açık link oluşturuldu", + "publicLinkCreated": "Herkese açık bağlantı oluşturuldu", "youCanManageYourLinksInTheShareTab": "Bağlantılarınızı paylaşım sekmesinden yönetebilirsiniz.", "linkCopiedToClipboard": "Link panoya kopyalandı", - "restore": "Yenile", + "restore": "Geri yükle", "@restore": { "description": "Display text for an action which triggers a restore of item from trash", "type": "text" @@ -781,7 +781,7 @@ "unarchive": "Arşivden cıkar", "favorite": "Favori", "removeFromFavorite": "Favorilerden Kaldır", - "shareLink": "Linki paylaş", + "shareLink": "Bağlantıyı paylaş", "createCollage": "Kolaj oluştur", "saveCollage": "Kolajı kaydet", "collageSaved": "Kolajınız galeriye kaydedildi", @@ -1281,6 +1281,8 @@ "createCollaborativeLink": "Ortak bağlantı oluşturun", "search": "Ara", "enterPersonName": "Kişi ismini giriniz", + "editEmailAlreadyLinked": "Bu e-posta zaten {name} kişisine bağlı.", + "viewPersonToUnlink": "Bağlantıyı kaldırmak için {name} kişisini görüntüle", "enterName": "İsim girin", "savePerson": "Kişiyi kaydet", "editPerson": "Kişiyi düzenle", @@ -1354,7 +1356,7 @@ "passwordStrengthInfo": "Parola gücü, parolanın uzunluğu, kullanılan karakterler ve parolanın en çok kullanılan ilk 10.000 parola arasında yer alıp almadığı dikkate alınarak hesaplanır", "noQuickLinksSelected": "Hızlı bağlantılar seçilmedi", "pleaseSelectQuickLinksToRemove": "Lütfen kaldırmak için hızlı bağlantıları seçin", - "removePublicLinks": "Herkese açık link oluştur", + "removePublicLinks": "Herkese açık bağlantıları kaldır", "thisWillRemovePublicLinksOfAllSelectedQuickLinks": "Bu, seçilen tüm hızlı bağlantıların genel bağlantılarını kaldıracaktır.", "guestView": "Misafir Görünümü", "guestViewEnablePreSteps": "Misafir görünümünü etkinleştirmek için lütfen sistem ayarlarınızda cihaz şifresi veya ekran kilidi ayarlayın.", @@ -1660,9 +1662,9 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Video akışı", + "videoStreaming": "Akışlandırılabilir videolar", "processingVideos": "Videolar işleniyor", - "streamDetails": "Yayın detayları", + "streamDetails": "Akış detayları", "processing": "İşleniyor", "queued": "Kuyrukta", "ineligible": "Uygun Değil", diff --git a/mobile/lib/l10n/intl_uk.arb b/mobile/lib/l10n/intl_uk.arb index 2410224eec..c13e19312c 100644 --- a/mobile/lib/l10n/intl_uk.arb +++ b/mobile/lib/l10n/intl_uk.arb @@ -439,7 +439,7 @@ "skip": "Пропустити", "updatingFolderSelection": "Оновлення вибору теки...", "itemCount": "{count, plural, one{{count} елемент} few {{count} елементи} many {{count} елементів} other{{count} елементів}}", - "deleteItemCount": "{count, plural, =1 {Видалено {count} елемент} few {Видалено {count} елементи} many {Видалено {count} елементів} other {Видалено {count} елементів}}", + "deleteItemCount": "{count, plural, =1 {Видалено {count} елемент} other {Видалено {count} елементів}}", "duplicateItemsGroup": "{count} файлів, кожен по {formattedSize}", "@duplicateItemsGroup": { "description": "Display the number of duplicate files and their size", @@ -456,7 +456,7 @@ } }, "showMemories": "Показати спогади", - "yearsAgo": "{count, plural, one{{count} рік тому} few {{count} роки тому} many {{count} років тому} other{{count} років тому}}", + "yearsAgo": "{count, plural, one{{count} рік тому} other{{count} років тому}}", "backupSettings": "Налаштування резервного копіювання", "backupStatus": "Стан резервного копіювання", "backupStatusDescription": "Елементи, для яких було створено резервну копію, показуватимуться тут", @@ -868,7 +868,7 @@ "authToViewYourMemories": "Авторизуйтеся, щоб переглянути ваші спогади", "unlock": "Розблокувати", "freeUpSpace": "Звільнити місце", - "filesBackedUpInAlbum": "{count, plural, one {Для 1 файлу} few {Для {formattedNumber} файлів} many {Для {formattedNumber} файлів} other {Для {formattedNumber} файлів}} у цьому альбомі було створено резервну копію", + "filesBackedUpInAlbum": "{count, plural, one {Для 1 файлу} other {Для {formattedNumber} файлів}} у цьому альбомі було створено резервну копію", "@filesBackedUpInAlbum": { "description": "Text to tell user how many files have been backed up in the album", "placeholders": { @@ -1354,16 +1354,6 @@ "enableMachineLearningBanner": "Увімкніть машинне навчання для магічного пошуку та розпізнавання облич", "searchDiscoverEmptySection": "Зображення будуть показані тут після завершення оброблення та синхронізації", "searchPersonsEmptySection": "Люди будуть показані тут після завершення оброблення та синхронізації", - "collaboratorsSuccessfullyAdded": "{count, plural, one {} few {Додано {count} співаторів} many {Додано {count} співаторів}=0 {Додано 0 співавторів} =1 {Додано 1 співавтор} other {Додано {count} співавторів}}", - "@collaboratorsSuccessfullyAdded": { - "placeholders": { - "count": { - "type": "int", - "example": "2" - } - }, - "description": "Number of collaborators that were successfully added to an album." - }, "accountIsAlreadyConfigured": "Обліковий запис уже налаштовано.", "sessionIdMismatch": "Невідповідність ідентифікатора сеансу", "@sessionIdMismatch": { diff --git a/mobile/lib/l10n/intl_zh.arb b/mobile/lib/l10n/intl_zh.arb index f9943cefbc..b2b4649b1e 100644 --- a/mobile/lib/l10n/intl_zh.arb +++ b/mobile/lib/l10n/intl_zh.arb @@ -1658,7 +1658,6 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "影音流", "processingVideos": "正在处理视频", "streamDetails": "流详情", "processing": "正在处理", diff --git a/mobile/lib/l10n/l10n.dart b/mobile/lib/l10n/l10n.dart index 5154f8840c..4ed4ff7fe3 100644 --- a/mobile/lib/l10n/l10n.dart +++ b/mobile/lib/l10n/l10n.dart @@ -29,21 +29,51 @@ const List appSupportedLocales = [ Locale("zh", "CN"), ]; +List _onDeviceLocales = []; Locale? autoDetectedLocale; -Locale localResolutionCallBack(locales, supportedLocales) { - for (Locale locale in locales) { - for (Locale supportedLocale in appSupportedLocales) { - if (supportedLocale == locale) { - autoDetectedLocale = supportedLocale; - return supportedLocale; - } else if (supportedLocale.languageCode == locale.languageCode) { - autoDetectedLocale = supportedLocale; - return supportedLocale; - } +Locale localResolutionCallBack(deviceLocales, supportedLocales) { + _onDeviceLocales = deviceLocales; + final Set languageSupport = {}; + for (Locale supportedLocale in appSupportedLocales) { + languageSupport.add(supportedLocale.languageCode); + } + for (Locale locale in deviceLocales) { + // check if exact local is supported, if yes, return it + if (appSupportedLocales.contains(locale)) { + autoDetectedLocale = locale; + return locale; + } + // check if language code is supported, if yes, return it + if (languageSupport.contains(locale.languageCode)) { + autoDetectedLocale = locale; + return locale; } } - return const Locale('en'); + // Return the first language code match or default to 'en' + return autoDetectedLocale ?? const Locale('en'); +} + +// This is used to get locale that should be used for various formatting +// operations like date, time, number etc. For common languages like english, different +// locale might have different formats. For example, en_US and en_GB have different +// formats for date and time. Use this method to find the best locale for formatting +// operations. This is not used for displaying text in the app. +Future getFormatLocale() async { + final Locale locale = (await getLocale())!; + Locale? firstLanguageMatch; + // see if exact matche is present in the device locales + for (Locale deviceLocale in _onDeviceLocales) { + if (deviceLocale.languageCode == locale.languageCode && + deviceLocale.countryCode == locale.countryCode) { + return deviceLocale; + } + if (firstLanguageMatch == null && + deviceLocale.languageCode == locale.languageCode) { + firstLanguageMatch = deviceLocale; + } + } + return firstLanguageMatch ?? locale; } Future getLocale({ diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 30a33e955e..d026952126 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -31,7 +31,6 @@ import "package:photos/services/account/user_service.dart"; import 'package:photos/services/app_lifecycle_service.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/services/favorites_service.dart'; -import "package:photos/services/filedata/filedata_service.dart"; import 'package:photos/services/home_widget_service.dart'; import 'package:photos/services/local_file_update_service.dart'; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; @@ -226,7 +225,7 @@ Future _init(bool isBackground, {String via = ''}) async { _logger.info("NetworkClient init done $tlog"); ServiceLocator.instance - .init(preferences, NetworkClient.instance.enteDio, packageInfo); + .init(preferences, NetworkClient.instance.enteDio,NetworkClient.instance.getDio(), packageInfo); _logger.info("UserService init $tlog"); await UserService.instance.init(); @@ -239,7 +238,6 @@ Future _init(bool isBackground, {String via = ''}) async { FavoritesService.instance.initFav().ignore(); LocalFileUpdateService.instance.init(preferences); SearchService.instance.init(); - FileDataService.instance.init(preferences); _logger.info("FileUploader init $tlog"); await FileUploader.instance.init(preferences, isBackground); @@ -256,7 +254,7 @@ Future _init(bool isBackground, {String via = ''}) async { await SyncService.instance.init(preferences); _logger.info("SyncService init done $tlog"); - await HomeWidgetService.instance.init(preferences); + HomeWidgetService.instance.init(preferences); if (!isBackground) { await _scheduleFGHomeWidgetSync(); diff --git a/mobile/lib/models/base/id.dart b/mobile/lib/models/base/id.dart index 2b095e0274..e275d298ad 100644 --- a/mobile/lib/models/base/id.dart +++ b/mobile/lib/models/base/id.dart @@ -2,16 +2,16 @@ import 'package:nanoid/nanoid.dart'; const enteWhiteListedAlphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; -const clusterIDLength = 22; +const randomIDLength = 22; String newClusterID() { - return "cluster_${customAlphabet(enteWhiteListedAlphabet, clusterIDLength)}"; + return "cluster_${customAlphabet(enteWhiteListedAlphabet, randomIDLength)}"; } String newID(String prefix) { - return "${prefix}_${customAlphabet(enteWhiteListedAlphabet, clusterIDLength)}"; + return "${prefix}_${customAlphabet(enteWhiteListedAlphabet, randomIDLength)}"; } String newIsolateTaskID(String task) { - return "${task}_${customAlphabet(enteWhiteListedAlphabet, clusterIDLength)}"; + return "${task}_${customAlphabet(enteWhiteListedAlphabet, randomIDLength)}"; } diff --git a/mobile/lib/models/collection/collection.dart b/mobile/lib/models/collection/collection.dart index fc00570e68..e51d3972bf 100644 --- a/mobile/lib/models/collection/collection.dart +++ b/mobile/lib/models/collection/collection.dart @@ -1,6 +1,8 @@ import 'dart:core'; import 'package:flutter/foundation.dart'; +import "package:photos/core/configuration.dart"; +import "package:photos/extensions/user_extension.dart"; import "package:photos/models/api/collection/public_url.dart"; import "package:photos/models/api/collection/user.dart"; import "package:photos/models/metadata/collection_magic.dart"; @@ -61,7 +63,14 @@ class Collection { set sharedMagicMetadata(ShareeMagicMetadata? val) => _sharedMmd = val; // ignore: deprecated_member_use_from_same_package - String get displayName => decryptedName ?? name ?? "Unnamed Album"; + String get displayName { + if (!isDeleted && + type == CollectionType.favorites && + !isOwner(Configuration.instance.getUserID() ?? -1)) { + return '${owner.nameOrEmail}\'s favorites'; + } + return decryptedName ?? name ?? "Unnamed Album"; + } // set the value for both name and decryptedName till we finish migration void setName(String newName) { diff --git a/mobile/lib/models/file/file.dart b/mobile/lib/models/file/file.dart index ca0ef1031d..8f55363277 100644 --- a/mobile/lib/models/file/file.dart +++ b/mobile/lib/models/file/file.dart @@ -4,12 +4,11 @@ import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart'; import 'package:photo_manager/photo_manager.dart'; -import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/models/file/file_type.dart'; import 'package:photos/models/location/location.dart'; import "package:photos/models/metadata/file_magic.dart"; -import "package:photos/service_locator.dart"; +import "package:photos/module/download/file_url.dart"; import 'package:photos/utils/exif_util.dart'; import 'package:photos/utils/file_uploader_util.dart'; import "package:photos/utils/panorama_util.dart"; @@ -242,47 +241,8 @@ class EnteFile { return metadata; } - String get downloadUrl { - if (localFileServer.isNotEmpty) { - return "$localFileServer/$uploadedFileID"; - } - final endpoint = Configuration.instance.getHttpEndpoint(); - if (endpoint != kDefaultProductionEndpoint || flagService.disableCFWorker) { - return endpoint + "/files/download/" + uploadedFileID.toString(); - } else { - return "https://files.ente.io/?fileID=" + uploadedFileID.toString(); - } - } - - String get publicDownloadUrl { - if (localFileServer.isNotEmpty) { - return "$localFileServer/$uploadedFileID"; - } - final endpoint = Configuration.instance.getHttpEndpoint(); - if (endpoint != kDefaultProductionEndpoint || flagService.disableCFWorker) { - return endpoint + - "/public-collection/files/download/" + - uploadedFileID.toString(); - } else { - return "https://public-albums.ente.io/download/?fileID=" + - uploadedFileID.toString(); - } - } - - String get pubPreviewUrl { - if (localFileServer.isNotEmpty) { - return "$localFileServer/thumb/$uploadedFileID"; - } - final endpoint = Configuration.instance.getHttpEndpoint(); - if (endpoint != kDefaultProductionEndpoint || flagService.disableCFWorker) { - return endpoint + - "/public-collection/files/preview/" + - uploadedFileID.toString(); - } else { - return "https://public-albums.ente.io/preview/?fileID=" + - uploadedFileID.toString(); - } - } + String get downloadUrl => + FileUrl.getUrl(uploadedFileID!, FileUrlType.download); String? get caption { return pubMagicMetadata?.caption; @@ -290,18 +250,6 @@ class EnteFile { String? debugCaption; - String get thumbnailUrl { - if (localFileServer.isNotEmpty) { - return "$localFileServer/thumb/$uploadedFileID"; - } - final endpoint = Configuration.instance.getHttpEndpoint(); - if (endpoint != kDefaultProductionEndpoint || flagService.disableCFWorker) { - return endpoint + "/files/preview/" + uploadedFileID.toString(); - } else { - return "https://thumbnails.ente.io/?fileID=" + uploadedFileID.toString(); - } - } - String get displayName { if (pubMagicMetadata != null && pubMagicMetadata!.editedName != null) { return pubMagicMetadata!.editedName!; diff --git a/mobile/lib/models/memories/filler_memory.dart b/mobile/lib/models/memories/filler_memory.dart index 7fd597980c..e8e8293415 100644 --- a/mobile/lib/models/memories/filler_memory.dart +++ b/mobile/lib/models/memories/filler_memory.dart @@ -10,8 +10,8 @@ class FillerMemory extends SmartMemory { this.yearsAgo, int firstDateToShow, int lastDateToShow, { - int? firstCreationTime, - int? lastCreationTime, + super.firstCreationTime, + super.lastCreationTime, }) : super( memories, MemoryType.filler, diff --git a/mobile/lib/models/memories/memories_cache.dart b/mobile/lib/models/memories/memories_cache.dart index 7a5ba47507..8257224fe7 100644 --- a/mobile/lib/models/memories/memories_cache.dart +++ b/mobile/lib/models/memories/memories_cache.dart @@ -1,5 +1,6 @@ import "dart:convert"; +import "package:photos/models/base/id.dart"; import "package:photos/models/base_location.dart"; import "package:photos/models/file/file.dart"; import "package:photos/models/location/location.dart"; @@ -83,6 +84,7 @@ class ToShowMemory { final int firstTimeToShow; final int lastTimeToShow; final int calculationTime; + final String id; final String? personID; final PeopleMemoryType? peopleMemoryType; @@ -99,7 +101,7 @@ class ToShowMemory { final relevantForNow = now >= firstTimeToShow && now < lastTimeToShow; final calculatedForNow = (now >= calculationTime) && (now < calculationTime + kMemoriesUpdateFrequency.inMicroseconds); - return relevantForNow && calculatedForNow; + return relevantForNow && (calculatedForNow || type == MemoryType.onThisDay); } ToShowMemory( @@ -108,6 +110,7 @@ class ToShowMemory { this.type, this.firstTimeToShow, this.lastTimeToShow, + this.id, this.calculationTime, { this.personID, this.peopleMemoryType, @@ -147,6 +150,7 @@ class ToShowMemory { memory.type, memory.firstDateToShow, memory.lastDateToShow, + memory.id, calcTime.microsecondsSinceEpoch, personID: personID, peopleMemoryType: peopleMemoryType, @@ -162,6 +166,7 @@ class ToShowMemory { memoryTypeFromString(json['type']), json['firstTimeToShow'], json['lastTimeToShow'], + json['id'] ?? newID(json['type'] as String), json['calculationTime'], personID: json['personID'], peopleMemoryType: json['peopleMemoryType'] != null @@ -186,6 +191,7 @@ class ToShowMemory { 'type': type.toString().split('.').last, 'firstTimeToShow': firstTimeToShow, 'lastTimeToShow': lastTimeToShow, + 'id': id, 'calculationTime': calculationTime, 'personID': personID, 'peopleMemoryType': peopleMemoryType?.toString().split('.').last, diff --git a/mobile/lib/models/memories/on_this_day_memory.dart b/mobile/lib/models/memories/on_this_day_memory.dart new file mode 100644 index 0000000000..de9e0e7ae3 --- /dev/null +++ b/mobile/lib/models/memories/on_this_day_memory.dart @@ -0,0 +1,24 @@ +import "package:photos/generated/l10n.dart"; +import "package:photos/models/memories/memory.dart"; +import "package:photos/models/memories/smart_memory.dart"; + +class OnThisDayMemory extends SmartMemory { + OnThisDayMemory( + List memories, + int firstDateToShow, + int lastDateToShow, { + super.firstCreationTime, + super.lastCreationTime, + }) : super( + memories, + MemoryType.onThisDay, + '', + firstDateToShow, + lastDateToShow, + ); + + @override + String createTitle(S s, String languageCode) { + return s.onThisDay; + } +} diff --git a/mobile/lib/models/memories/smart_memory.dart b/mobile/lib/models/memories/smart_memory.dart index b05ff64959..52ce8fbf64 100644 --- a/mobile/lib/models/memories/smart_memory.dart +++ b/mobile/lib/models/memories/smart_memory.dart @@ -1,4 +1,5 @@ import "package:photos/generated/l10n.dart"; +import "package:photos/models/base/id.dart"; import "package:photos/models/memories/memory.dart"; enum MemoryType { @@ -7,6 +8,7 @@ enum MemoryType { clip, time, filler, + onThisDay, } MemoryType memoryTypeFromString(String type) { @@ -21,6 +23,8 @@ MemoryType memoryTypeFromString(String type) { return MemoryType.filler; case "clip": return MemoryType.clip; + case "onThisDay": + return MemoryType.onThisDay; default: throw ArgumentError("Invalid memory type: $type"); } @@ -32,6 +36,7 @@ class SmartMemory { String title; int firstDateToShow; int lastDateToShow; + late final String id; int? firstCreationTime; int? lastCreationTime; @@ -42,9 +47,12 @@ class SmartMemory { this.title, this.firstDateToShow, this.lastDateToShow, { + String? id, this.firstCreationTime, this.lastCreationTime, - }); + }) { + this.id = id ?? newID(type.name); + } bool get notForShow => firstDateToShow == 0 && lastDateToShow == 0; diff --git a/mobile/lib/models/memories/time_memory.dart b/mobile/lib/models/memories/time_memory.dart index e723224aba..213ccba7c5 100644 --- a/mobile/lib/models/memories/time_memory.dart +++ b/mobile/lib/models/memories/time_memory.dart @@ -16,8 +16,8 @@ class TimeMemory extends SmartMemory { this.day, this.month, this.yearsAgo, - int? firstCreationTime, - int? lastCreationTime, + super.firstCreationTime, + super.lastCreationTime, }) : super( memories, MemoryType.time, diff --git a/mobile/lib/models/metadata/file_magic.dart b/mobile/lib/models/metadata/file_magic.dart index 02f6188a9d..53627aafbc 100644 --- a/mobile/lib/models/metadata/file_magic.dart +++ b/mobile/lib/models/metadata/file_magic.dart @@ -9,6 +9,7 @@ const captionKey = "caption"; const uploaderNameKey = "uploaderName"; const widthKey = 'w'; const heightKey = 'h'; +const streamVersionKey = 'sv'; const mediaTypeKey = 'mediaType'; const latKey = "lat"; const longKey = "long"; @@ -48,6 +49,10 @@ class PubMagicMetadata { double? lat; double? long; + // Indicates streaming version of the file. + // If this is set, then the file is a streaming version of the original file. + int? sv; + // ISO 8601 datetime without timezone. This contains the date and time of the photo in the original tz // where the photo was taken. String? dateTime; @@ -83,6 +88,7 @@ class PubMagicMetadata { this.mediaType, this.dateTime, this.offsetTime, + this.sv, }); factory PubMagicMetadata.fromEncodedJson(String encodedJson) => @@ -107,6 +113,7 @@ class PubMagicMetadata { mediaType: map[mediaTypeKey], dateTime: map[dateTimeKey], offsetTime: map[offsetTimeKey], + sv: safeParseInt(map[streamVersionKey], streamVersionKey), ); } diff --git a/mobile/lib/models/preview/playlist_data.dart b/mobile/lib/models/preview/playlist_data.dart index e5628dc131..7f40531017 100644 --- a/mobile/lib/models/preview/playlist_data.dart +++ b/mobile/lib/models/preview/playlist_data.dart @@ -1,5 +1,3 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; import "dart:io"; class PlaylistData { @@ -7,56 +5,13 @@ class PlaylistData { int? width; int? height; int? size; + int? durationInSeconds; PlaylistData({ required this.preview, this.width, this.height, this.size, + this.durationInSeconds, }); - - PlaylistData copyWith({ - File? preview, - int? width, - int? height, - int? size, - }) { - return PlaylistData( - preview: preview ?? this.preview, - width: width ?? this.width, - height: height ?? this.height, - size: size ?? this.size, - ); - } - - Map toMap() { - return { - 'preview': preview.readAsStringSync(), - 'width': width, - 'height': height, - 'size': size, - }; - } - - String toJson() => json.encode(toMap()); - - @override - String toString() { - return 'PlaylistData(preview: $preview, width: $width, height: $height, size: $size)'; - } - - @override - bool operator ==(covariant PlaylistData other) { - if (identical(this, other)) return true; - - return other.preview == preview && - other.width == width && - other.height == height && - other.size == size; - } - - @override - int get hashCode { - return preview.hashCode ^ width.hashCode ^ height.hashCode ^ size.hashCode; - } } diff --git a/mobile/lib/models/search/search_constants.dart b/mobile/lib/models/search/search_constants.dart index 60c178192b..f05dfc50cb 100644 --- a/mobile/lib/models/search/search_constants.dart +++ b/mobile/lib/models/search/search_constants.dart @@ -3,3 +3,4 @@ const kPersonWidgetKey = 'person_widget_key'; const kClusterParamId = 'cluster_id'; const kFileID = 'file_id'; const kContactEmail = 'contact_email'; +const kContactCollections = 'contact_collections'; diff --git a/mobile/lib/models/search/search_types.dart b/mobile/lib/models/search/search_types.dart index db2c9be797..2a209cafc9 100644 --- a/mobile/lib/models/search/search_types.dart +++ b/mobile/lib/models/search/search_types.dart @@ -97,19 +97,14 @@ extension SectionTypeExtensions on SectionType { bool get isCTAVisible { switch (this) { case SectionType.face: - return false; case SectionType.magic: - return false; case SectionType.moment: - return false; - case SectionType.location: - return true; - case SectionType.contacts: - return true; - case SectionType.album: - return true; case SectionType.fileTypesAndExtension: return false; + case SectionType.location: + case SectionType.contacts: + case SectionType.album: + return true; } } @@ -117,24 +112,20 @@ extension SectionTypeExtensions on SectionType { bool get sortByName => this != SectionType.face && this != SectionType.magic && - this != SectionType.moment; + this != SectionType.moment && + this != SectionType.contacts; bool get isEmptyCTAVisible { switch (this) { case SectionType.face: - return false; case SectionType.magic: - return false; case SectionType.moment: - return false; - case SectionType.location: - return true; - case SectionType.contacts: - return true; - case SectionType.album: - return true; case SectionType.fileTypesAndExtension: return false; + case SectionType.location: + case SectionType.contacts: + case SectionType.album: + return true; } } @@ -236,21 +227,21 @@ extension SectionTypeExtensions on SectionType { } Future> getData( - BuildContext context, { + BuildContext? context, { int? limit, }) { switch (this) { case SectionType.face: return SearchService.instance.getAllFace(limit); case SectionType.magic: - return SearchService.instance.getMagicSectionResults(context); + return SearchService.instance.getMagicSectionResults(context!); case SectionType.moment: if (flagService.internalUser) { // TODO: lau: remove this whole smart memories and moment altogether - return SearchService.instance.smartMemories(context, limit); + return SearchService.instance.smartMemories(context!, limit); } - return SearchService.instance.getRandomMomentsSearchResults(context); + return SearchService.instance.getRandomMomentsSearchResults(context!); case SectionType.location: return SearchService.instance.getAllLocationTags(limit); @@ -263,7 +254,7 @@ extension SectionTypeExtensions on SectionType { case SectionType.fileTypesAndExtension: return SearchService.instance - .getAllFileTypesAndExtensionsResults(context, limit); + .getAllFileTypesAndExtensionsResults(context!, limit); } } @@ -288,6 +279,8 @@ extension SectionTypeExtensions on SectionType { return [Bus.instance.on()]; case SectionType.magic: return [Bus.instance.on()]; + case SectionType.contacts: + return [Bus.instance.on()]; default: return []; } diff --git a/mobile/lib/models/selected_albums.dart b/mobile/lib/models/selected_albums.dart new file mode 100644 index 0000000000..0a09bafe05 --- /dev/null +++ b/mobile/lib/models/selected_albums.dart @@ -0,0 +1,46 @@ +import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/foundation.dart'; +import 'package:photos/core/event_bus.dart'; +import "package:photos/events/clear_album_selections_event.dart"; +import 'package:photos/models/collection/collection.dart'; + +class SelectedAlbums extends ChangeNotifier { + final albums = {}; + + void toggleSelection(Collection albumToToggle) { + final Collection? alreadySelected = albums.firstWhereOrNull( + (element) => element.id == albumToToggle.id, + ); + if (alreadySelected != null) { + albums.remove(alreadySelected); + } else { + albums.add(albumToToggle); + } + notifyListeners(); + } + + void select(Set albumsToSelect) { + albums.addAll(albumsToSelect); + notifyListeners(); + } + + void unSelect( + Set albumsToUnselect, { + bool skipNotify = false, + }) { + albums.removeWhere((album) => albumsToUnselect.contains(album)); + if (!skipNotify) { + notifyListeners(); + } + } + + bool isAlbumSelected(Collection album) { + return albums.any((element) => element.id == album.id); + } + + void clearAll() { + Bus.instance.fire(ClearAlbumSelectionsEvent()); + albums.clear(); + notifyListeners(); + } +} diff --git a/mobile/lib/models/selected_people.dart b/mobile/lib/models/selected_people.dart new file mode 100644 index 0000000000..f9ba503d7d --- /dev/null +++ b/mobile/lib/models/selected_people.dart @@ -0,0 +1,42 @@ +import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/foundation.dart'; + +class SelectedPeople extends ChangeNotifier { + final personIds = {}; + + void toggleSelection(String personID) { + final String? alreadySelected = personIds.firstWhereOrNull( + (element) => element == personID, + ); + if (alreadySelected != null) { + personIds.remove(alreadySelected); + } else { + personIds.add(personID); + } + notifyListeners(); + } + + void select(Set personToSelect) { + personIds.addAll(personToSelect); + notifyListeners(); + } + + void unSelect( + Set peopleToUnselect, { + bool skipNotify = false, + }) { + personIds.removeWhere((personID) => peopleToUnselect.contains(personID)); + if (!skipNotify) { + notifyListeners(); + } + } + + bool isPersonSelected(String personId) { + return personIds.any((element) => element == personId); + } + + void clearAll() { + personIds.clear(); + notifyListeners(); + } +} diff --git a/mobile/lib/module/download/file_url.dart b/mobile/lib/module/download/file_url.dart new file mode 100644 index 0000000000..e5faced127 --- /dev/null +++ b/mobile/lib/module/download/file_url.dart @@ -0,0 +1,43 @@ +import "package:photos/core/configuration.dart"; +import "package:photos/core/constants.dart"; +import "package:photos/service_locator.dart"; + +enum FileUrlType { + download, + publicDownload, + thumbnail, + publicThumbnail, + directDownload, +} + +class FileUrl { + static String getUrl(int fileID, FileUrlType type) { + final endpoint = Configuration.instance.getHttpEndpoint(); + final disableWorker = + endpoint != kDefaultProductionEndpoint || flagService.disableCFWorker; + + switch (type) { + case FileUrlType.directDownload: + return "$endpoint/files/download/$fileID"; + case FileUrlType.download: + return disableWorker + ? "$endpoint/files/download/$fileID" + : "https://files.ente.io/?fileID=$fileID"; + + case FileUrlType.publicDownload: + return disableWorker + ? "$endpoint/public-collection/files/download/$fileID" + : "https://public-albums.ente.io/download/?fileID=$fileID"; + + case FileUrlType.thumbnail: + return disableWorker + ? "$endpoint/files/preview/$fileID" + : "https://thumbnails.ente.io/?fileID=$fileID"; + + case FileUrlType.publicThumbnail: + return disableWorker + ? "$endpoint/public-collection/files/preview/$fileID" + : "https://public-albums.ente.io/preview/?fileID=$fileID"; + } + } +} diff --git a/mobile/lib/module/download/manager.dart b/mobile/lib/module/download/manager.dart new file mode 100644 index 0000000000..614ab23214 --- /dev/null +++ b/mobile/lib/module/download/manager.dart @@ -0,0 +1,375 @@ +import "dart:async"; +import "dart:io"; + +import "package:dio/dio.dart"; +import "package:logging/logging.dart"; +import "package:photos/core/configuration.dart"; + +import "package:photos/module/download/file_url.dart"; +import "package:photos/module/download/task.dart"; +import "package:photos/service_locator.dart"; + +class DownloadManager { + final _logger = Logger('DownloadManager'); + static const int downloadChunkSize = 40 * 1024 * 1024; + + final Dio _dio; + + // In-memory storage for download tasks + final Map _tasks = {}; + + // Active downloads with their completers and streams + final Map> _completers = {}; + final Map> _streams = {}; + final Map _cancelTokens = {}; + + DownloadManager(this._dio); + + /// Subscribe to download progress updates for a specific file ID + Stream watchDownload(int fileId) { + _streams[fileId] ??= StreamController.broadcast(); + return _streams[fileId]!.stream; + } + + bool enableResumableDownload(int? size) { + if (size == null) return false; + //todo: Use FileUrlType.direct instead of FileUrlType.directDownload + return size > downloadChunkSize && flagService.internalUser; + } + + /// Start download and return a Future that completes when download finishes + /// If download was paused, calling this again will resume it + Future download( + int fileId, + String filename, + int totalBytes, + ) async { + // If already downloading, return existing future + if (_completers.containsKey(fileId)) { + return _completers[fileId]!.future; + } + + final completer = Completer(); + _completers[fileId] = completer; + + // Get or create task + final existingTask = _tasks[fileId]; + final task = existingTask ?? + DownloadTask( + id: fileId, + filename: filename, + totalBytes: totalBytes, + ); + + // Store task in memory + _tasks[fileId] = task; + + // Don't restart if already completed + if (task.isCompleted) { + // ensure that the file exists + final filePath = task.filePath; + if (filePath == null || !(await File(filePath).exists())) { + // If the file doesn't exist, mark the task as error + _logger.warning( + 'File not found for ${task.filename} (${task.bytesDownloaded}/${task.totalBytes} bytes)', + ); + final updatedTask = task.copyWith( + status: DownloadStatus.error, + error: 'File not found', + filePath: null, + ); + _updateTask(updatedTask); + final result = DownloadResult(updatedTask, false); + completer.complete(result); + return result; + } else { + _logger.info( + 'Download already completed for ${task.filename} (${task.bytesDownloaded}/${task.totalBytes} bytes)', + ); + final result = DownloadResult(task, true); + completer.complete(result); + return result; + } + } + unawaited(_startDownload(task, completer)); + return completer.future; + } + + /// Pause download + Future pause(int fileId) async { + final token = _cancelTokens[fileId]; + if (token != null && !token.isCancelled) { + token.cancel('paused'); + } + + final task = _tasks[fileId]; + if (task != null && task.isActive) { + _updateTask(task.copyWith(status: DownloadStatus.paused)); + } + + // Clean up streams if no listeners + final stream = _streams[fileId]; + if (stream != null && !stream.hasListener) { + await stream.close(); + _streams.remove(fileId); + } + } + + /// Cancel and delete download + Future cancel(int fileId) async { + final token = _cancelTokens[fileId]; + if (token != null && !token.isCancelled) { + token.cancel('cancelled'); + } + + final task = _tasks[fileId]; + if (task != null) { + await _deleteFiles(task); + _updateTask(task.copyWith(status: DownloadStatus.cancelled)); + _tasks.remove(fileId); + } + _cleanup(fileId); + } + + /// Get current download status + Future getDownload(int fileId) async => _tasks[fileId]; + + /// Get all downloads + Future> getAllDownloads() async => _tasks.values.toList(); + + Future _startDownload( + DownloadTask task, + Completer completer, + ) async { + try { + task = task.copyWith(status: DownloadStatus.downloading); + _updateTask(task); + + final cancelToken = CancelToken(); + _cancelTokens[task.id] = cancelToken; + + final directory = Configuration.instance.getTempDirectory(); + final basePath = '$directory${task.id}.encrypted'; + + // Check existing chunks and calculate progress + final totalChunks = (task.totalBytes / downloadChunkSize).ceil(); + final existingChunks = + await _validateExistingChunks(basePath, task.totalBytes, totalChunks); + + task = task.copyWith( + bytesDownloaded: _calculateDownloadedBytes( + existingChunks, + task.totalBytes, + totalChunks, + ), + ); + _updateTask(task); + + _logger.info( + 'Resuming download for ${task.filename} (${task.bytesDownloaded}/${task.totalBytes} bytes)', + ); + for (int i = 0; i < totalChunks; i++) { + if (existingChunks[i] || cancelToken.isCancelled) continue; + _logger.info('Downloading chunk ${i + 1} of $totalChunks'); + await _downloadChunk(task, basePath, i, totalChunks, cancelToken); + existingChunks[i] = true; + } + + if (!cancelToken.isCancelled) { + final finalPath = await _combineChunks(basePath, totalChunks); + task = task.copyWith( + status: DownloadStatus.completed, + filePath: finalPath, + bytesDownloaded: task.totalBytes, + ); + _updateTask(task); + completer.complete(DownloadResult(task, true)); + } + } catch (e) { + if (e is DioException && e.type == DioExceptionType.cancel) { + // Complete future with current task state (paused or cancelled) + final currentTask = _tasks[task.id]; + if (currentTask != null && !completer.isCompleted) { + completer.complete(DownloadResult(currentTask, false)); + } + return; + } + + task = task.copyWith(status: DownloadStatus.error, error: e.toString()); + _updateTask(task); + if (!completer.isCompleted) { + completer.complete(DownloadResult(task, false)); + } + } finally { + _cleanup(task.id); + } + } + + String _getChunkPath(String basePath, int part) { + return '$basePath.${part}_part'; + } + + Future> _validateExistingChunks( + String basePath, + int totalBytes, + int totalChunks, + ) async { + final existingChunks = List.filled(totalChunks, false); + + for (int i = 0; i < totalChunks; i++) { + final chunkFile = File(_getChunkPath(basePath, i + 1)); + if (!await chunkFile.exists()) continue; + + final expectedSize = i == totalChunks - 1 + ? totalBytes - (i * downloadChunkSize) + : downloadChunkSize; + + final actualSize = await chunkFile.length(); + if (actualSize == expectedSize) { + _logger.info('existing chunk ${i + 1} is valid'); + existingChunks[i] = true; + } else { + _logger.warning( + 'Chunk ${i + 1} is corrupted: expected $expectedSize bytes, ' + 'but got $actualSize bytes', + ); + existingChunks[i] = false; + await chunkFile.delete(); // Remove corrupted chunk + } + } + + return existingChunks; + } + + int _calculateDownloadedBytes( + List existingChunks, + int totalBytes, + int totalChunks, + ) { + int bytes = 0; + for (int i = 0; i < existingChunks.length; i++) { + if (existingChunks[i]) { + bytes += i == totalChunks - 1 + ? totalBytes - (i * downloadChunkSize) + : downloadChunkSize; + } + } + return bytes; + } + + Future _downloadChunk( + DownloadTask task, + String basePath, + int chunkIndex, + int totalChunks, + CancelToken cancelToken, + ) async { + final chunkPath = _getChunkPath(basePath, chunkIndex + 1); + final startByte = chunkIndex * downloadChunkSize; + final endByte = chunkIndex == totalChunks - 1 + ? task.totalBytes - 1 + : (startByte + downloadChunkSize) - 1; + + await _dio.download( + FileUrl.getUrl(task.id, FileUrlType.directDownload), + chunkPath, + options: Options( + headers: { + "X-Auth-Token": Configuration.instance.getToken(), + "Range": "bytes=$startByte-$endByte", + }, + ), + cancelToken: cancelToken, + onReceiveProgress: (received, total) async { + final updatedTask = task.copyWith( + bytesDownloaded: (chunkIndex) * downloadChunkSize + received, + ); + _notifyProgress(updatedTask); + }, + ); + // Update progress after chunk completion + final chunkFileSize = await File(chunkPath).length(); + task = task.copyWith( + bytesDownloaded: (chunkIndex) * downloadChunkSize + chunkFileSize, + ); + _updateTask(task); + } + + Future _combineChunks(String basePath, int totalChunks) async { + final finalFile = File(basePath); + final sink = finalFile.openWrite(); + try { + for (int i = 1; i <= totalChunks; i++) { + final chunkFile = File(_getChunkPath(basePath, i)); + final bytes = await chunkFile.readAsBytes(); + sink.add(bytes); + await chunkFile.delete(); + } + } finally { + await sink.close(); + } + return finalFile.path; + } + + Future _deleteFiles(DownloadTask task) async { + try { + final directory = Configuration.instance.getTempDirectory(); + final basePath = '$directory${task.id}.encrypted'; + final finalFile = File(basePath); + if (await finalFile.exists()) await finalFile.delete(); + + // Delete chunk files + final totalChunks = (task.totalBytes / downloadChunkSize).ceil(); + for (int i = 1; i <= totalChunks; i++) { + final chunkFile = File(_getChunkPath(basePath, i)); + if (await chunkFile.exists()) await chunkFile.delete(); + } + } catch (e) { + _logger.warning('Error deleting files: $e'); + } + } + + void _updateTask(DownloadTask task) { + _tasks[task.id] = task; + _notifyProgress(task); + } + + void _notifyProgress(DownloadTask task) { + final stream = _streams[task.id]; + if (stream != null && !stream.isClosed) { + stream.add(task); + } + } + + void _cleanup(int fileId) { + _completers.remove(fileId); + _cancelTokens.remove(fileId); + + final stream = _streams[fileId]; + if (stream != null && !stream.hasListener) { + stream.close(); + _streams.remove(fileId); + } + } + + Future dispose() async { + for (final completer in _completers.values) { + if (!completer.isCompleted) { + completer.completeError('Disposed'); + } + } + _completers.clear(); + + for (final token in _cancelTokens.values) { + token.cancel('Disposed'); + } + _cancelTokens.clear(); + + for (final stream in _streams.values) { + await stream.close(); + } + _streams.clear(); + + _tasks.clear(); + } +} diff --git a/mobile/lib/module/download/task.dart b/mobile/lib/module/download/task.dart new file mode 100644 index 0000000000..1bd96ca295 --- /dev/null +++ b/mobile/lib/module/download/task.dart @@ -0,0 +1,80 @@ +enum DownloadStatus { + pending, + downloading, + paused, + completed, + error, + cancelled +} + +class DownloadTask { + final int id; + final String filename; + final int totalBytes; + int bytesDownloaded; + DownloadStatus status; + String? error; + String? filePath; + + DownloadTask({ + required this.id, + required this.filename, + required this.totalBytes, + this.bytesDownloaded = 0, + this.status = DownloadStatus.pending, + this.error, + this.filePath, + }); + + double get progress => totalBytes > 0 ? bytesDownloaded / totalBytes : 0.0; + bool get isCompleted => status == DownloadStatus.completed; + bool get isActive => status == DownloadStatus.downloading; + bool get isFinished => [ + DownloadStatus.completed, + DownloadStatus.error, + DownloadStatus.cancelled, + ].contains(status); + + Map toMap() => { + 'id': id, + 'filename': filename, + 'totalBytes': totalBytes, + 'bytesDownloaded': bytesDownloaded, + 'status': status.name, + 'error': error, + 'filePath': filePath, + }; + + static DownloadTask fromMap(Map map) => DownloadTask( + id: map['id'], + filename: map['filename'], + totalBytes: map['totalBytes'], + bytesDownloaded: map['bytesDownloaded'] ?? 0, + status: DownloadStatus.values.byName(map['status']), + error: map['error'], + filePath: map['filePath'], + ); + + DownloadTask copyWith({ + int? bytesDownloaded, + DownloadStatus? status, + String? error, + String? filePath, + }) => + DownloadTask( + id: id, + filename: filename, + totalBytes: totalBytes, + bytesDownloaded: bytesDownloaded ?? this.bytesDownloaded, + status: status ?? this.status, + error: error ?? this.error, + filePath: filePath ?? this.filePath, + ); +} + +class DownloadResult { + final DownloadTask task; + final bool success; + + DownloadResult(this.task, this.success); +} diff --git a/mobile/lib/service_locator.dart b/mobile/lib/service_locator.dart index 57728a7a26..84e5eb6a18 100644 --- a/mobile/lib/service_locator.dart +++ b/mobile/lib/service_locator.dart @@ -4,8 +4,10 @@ import "package:ente_cast_normal/ente_cast_normal.dart"; import "package:ente_feature_flag/ente_feature_flag.dart"; import "package:package_info_plus/package_info_plus.dart"; import "package:photos/gateways/entity_gw.dart"; +import "package:photos/module/download/manager.dart"; import "package:photos/services/account/billing_service.dart"; import "package:photos/services/entity_service.dart"; +import "package:photos/services/filedata/filedata_service.dart"; import "package:photos/services/location_service.dart"; import "package:photos/services/machine_learning/face_ml/face_recognition_service.dart"; import "package:photos/services/machine_learning/machine_learning_controller.dart"; @@ -22,6 +24,7 @@ import "package:shared_preferences/shared_preferences.dart"; class ServiceLocator { late final SharedPreferences prefs; late final Dio enteDio; + late final Dio nonEnteDio; late final PackageInfo packageInfo; // instance @@ -29,9 +32,15 @@ class ServiceLocator { static final ServiceLocator instance = ServiceLocator._privateConstructor(); - init(SharedPreferences prefs, Dio enteDio, PackageInfo packageInfo) { + init( + SharedPreferences prefs, + Dio enteDio, + Dio nonEnteDio, + PackageInfo packageInfo, + ) { this.prefs = prefs; this.enteDio = enteDio; + this.nonEnteDio = nonEnteDio; this.packageInfo = packageInfo; } } @@ -150,3 +159,20 @@ PermissionService get permissionService { _permissionService ??= PermissionService(ServiceLocator.instance.prefs); return _permissionService!; } + +FileDataService? _fileDataService; +FileDataService get fileDataService { + _fileDataService ??= FileDataService( + ServiceLocator.instance.prefs, + ServiceLocator.instance.enteDio, + ); + return _fileDataService!; +} + +DownloadManager? _downloadManager; +DownloadManager get downloadManager { + _downloadManager ??= DownloadManager( + ServiceLocator.instance.nonEnteDio, + ); + return _downloadManager!; +} diff --git a/mobile/lib/services/account/user_service.dart b/mobile/lib/services/account/user_service.dart index ab4801d94b..00f6842d97 100644 --- a/mobile/lib/services/account/user_service.dart +++ b/mobile/lib/services/account/user_service.dart @@ -1445,4 +1445,19 @@ class UserService { return emailIDs; } + + Set getEmailIDsOfFamilyMember() { + final emailIDs = {}; + + final cachedUserDetails = getCachedUserDetails(); + if (cachedUserDetails?.familyData?.members?.isNotEmpty ?? false) { + for (final member in cachedUserDetails!.familyData!.members!) { + if (member.email.isNotEmpty) { + emailIDs.add(member.email); + } + } + } + + return emailIDs; + } } diff --git a/mobile/lib/services/album_home_widget_service.dart b/mobile/lib/services/album_home_widget_service.dart new file mode 100644 index 0000000000..4bf8864940 --- /dev/null +++ b/mobile/lib/services/album_home_widget_service.dart @@ -0,0 +1,484 @@ +import 'dart:convert'; +import "dart:math"; + +import "package:collection/collection.dart"; +import 'package:crypto/crypto.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:logging/logging.dart'; +import 'package:photos/db/files_db.dart'; +import 'package:photos/models/collection/collection.dart'; +import 'package:photos/models/collection/collection_items.dart'; +import 'package:photos/models/file/file.dart'; +import 'package:photos/service_locator.dart'; +import 'package:photos/services/collections_service.dart'; +import 'package:photos/services/favorites_service.dart'; +import 'package:photos/services/home_widget_service.dart'; +import 'package:photos/services/sync/local_sync_service.dart'; +import "package:photos/ui/viewer/file/detail_page.dart"; +import 'package:photos/ui/viewer/gallery/collection_page.dart'; +import 'package:photos/utils/navigation_util.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:synchronized/synchronized.dart'; + +class AlbumHomeWidgetService { + // Constants + static const String SELECTED_ALBUMS_KEY = "selectedAlbumsHW"; + static const String ALBUMS_LAST_HASH_KEY = "albumsLastHash"; + static const String ANDROID_CLASS_NAME = "EnteAlbumsWidgetProvider"; + static const String IOS_CLASS_NAME = "EnteAlbumWidget"; + static const String ALBUMS_CHANGED_KEY = "albumsChanged.widget"; + static const String ALBUMS_STATUS_KEY = "albumsStatusKey.widget"; + static const String TOTAL_ALBUMS_KEY = "totalAlbums"; + static const int MAX_ALBUMS_LIMIT = 50; + + // Singleton pattern + static final AlbumHomeWidgetService instance = + AlbumHomeWidgetService._privateConstructor(); + AlbumHomeWidgetService._privateConstructor(); + + // Properties + final Logger _logger = Logger((AlbumHomeWidgetService).toString()); + late final SharedPreferences _prefs; + final _albumsForceRefreshLock = Lock(); + bool _hasSyncedAlbums = false; + + // Initialization + void init(SharedPreferences prefs) { + _prefs = prefs; + } + + // Public methods + Future initHomeWidget(bool? forceFetchNewAlbums) async { + if (await _hasAnyBlockers()) { + await clearWidget(); + return; + } + + await _albumsForceRefreshLock.synchronized(() async { + if (await _hasAnyBlockers()) { + await clearWidget(); + return; + } + + final isWidgetEmpty = await _isWidgetEmpty(); + forceFetchNewAlbums ??= await _shouldForceFetchAlbums(isWidgetEmpty); + + _logger.warning( + "Initializing albums widget: forceFetch: $forceFetchNewAlbums, isEmpty: $isWidgetEmpty", + ); + + if (forceFetchNewAlbums!) { + await _forceAlbumsUpdate(); + } else if (!isWidgetEmpty) { + await _syncExistingAlbums(); + } + }); + } + + List? getSelectedAlbumIds() { + final selectedAlbums = _prefs.getStringList(SELECTED_ALBUMS_KEY); + return selectedAlbums?.map((id) => int.tryParse(id) ?? 0).toList(); + } + + Future setSelectedAlbums(List selectedAlbums) async { + await _prefs.setStringList(SELECTED_ALBUMS_KEY, selectedAlbums); + } + + String? getAlbumsLastHash() { + return _prefs.getString(ALBUMS_LAST_HASH_KEY); + } + + Future setAlbumsLastHash(String hash) async { + await _prefs.setString(ALBUMS_LAST_HASH_KEY, hash); + } + + Future countHomeWidgets() async { + return await HomeWidgetService.instance.countHomeWidgets( + ANDROID_CLASS_NAME, + IOS_CLASS_NAME, + ); + } + + Future clearWidget() async { + if (await _isWidgetEmpty()) { + _logger.info("Widget already empty, nothing to clear"); + return; + } + + _logger.info("Clearing AlbumsHomeWidget"); + await _setTotalAlbums(null); + await updateAlbumsStatus(WidgetStatus.syncedEmpty); + _hasSyncedAlbums = false; + await _refreshWidget(message: "AlbumsHomeWidget cleared & updated"); + } + + bool getAlbumsChanged() { + return _prefs.getBool(ALBUMS_CHANGED_KEY) ?? false; + } + + Future updateAlbumsChanged(bool value) async { + _logger.info("Updating albums changed flag to $value"); + await _prefs.setBool(ALBUMS_CHANGED_KEY, value); + } + + WidgetStatus getAlbumsStatus() { + return WidgetStatus.values.firstWhereOrNull( + (v) => v.index == (_prefs.getInt(ALBUMS_STATUS_KEY) ?? 0), + ) ?? + WidgetStatus.notSynced; + } + + Future updateAlbumsStatus(WidgetStatus value) async { + await _prefs.setInt(ALBUMS_STATUS_KEY, value.index); + } + + Future checkPendingAlbumsSync({bool addDelay = true}) async { + if (addDelay) { + await Future.delayed(const Duration(seconds: 5)); + } + + final isWidgetEmpty = await _isWidgetEmpty(); + final shouldForceFetch = await _shouldForceFetchAlbums(isWidgetEmpty); + + if (_hasSyncedAlbums && !shouldForceFetch) { + _logger.info("Albums already synced, no action needed"); + return; + } + + await initHomeWidget(shouldForceFetch); + } + + Future albumsChanged() async { + final lastHash = getAlbumsLastHash(); + final selectedAlbumIds = await _getEffectiveSelectedAlbumIds(); + final currentHash = _calculateHash(selectedAlbumIds); + + if (selectedAlbumIds.isEmpty || currentHash == lastHash) { + _logger.info("No changes detected in albums"); + return; + } + + _logger.info("Albums changed, updating widget"); + await updateAlbumsChanged(true); + await initHomeWidget(true); + } + + List getAlbumsByIds(List albumIds) { + final albums = []; + + for (final albumId in albumIds) { + final collection = CollectionsService.instance.getCollectionByID(albumId); + if (collection != null) { + albums.add(collection); + } + } + + return albums; + } + + String _calculateHash(List albumIds) { + String updationTimestamps = ""; + + for (final albumId in albumIds) { + final collection = CollectionsService.instance.getCollectionByID(albumId); + if (collection != null) { + updationTimestamps += "${collection.updationTime.toString()}_"; + } + } + + final hash = md5 + .convert(utf8.encode(updationTimestamps)) + .toString() + .substring(0, 10); + return hash; + } + + Future onLaunchFromWidget( + int fileId, + int collectionId, + BuildContext context, + ) async { + _hasSyncedAlbums = true; + await _syncExistingAlbums(); + + final collection = + CollectionsService.instance.getCollectionByID(collectionId); + if (collection == null) { + _logger.warning( + "Cannot launch widget: collection with ID $collectionId not found", + ); + return; + } + + // First navigate to the collection page + final thumbnail = await CollectionsService.instance.getCover(collection); + routeToPage( + context, + CollectionPage( + CollectionWithThumbnail(collection, thumbnail), + ), + ).ignore(); + final getAllFilesCollection = + await FilesDB.instance.getAllFilesCollection(collection.id); + + // Then open the specific file + final file = await FilesDB.instance.getFile(fileId); + if (file == null) { + _logger.warning("Cannot launch widget: file with ID $fileId not found"); + return; + } + + await routeToPage( + context, + DetailPage( + DetailPageConfiguration( + getAllFilesCollection, + getAllFilesCollection.indexOf(file), + "albumwidget", + ), + ), + forceCustomPageRoute: true, + ); + } + + // Private methods + Future _hasAnyBlockers() async { + // Check if first import is completed + final hasCompletedFirstImport = + LocalSyncService.instance.hasCompletedFirstImport(); + if (!hasCompletedFirstImport) { + _logger.warning("First import not completed"); + return true; + } + + // Check if selected albums exist + final selectedAlbumIds = getSelectedAlbumIds(); + final albums = getAlbumsByIds(selectedAlbumIds ?? []); + + if ((selectedAlbumIds?.isNotEmpty ?? false) && albums.isEmpty) { + _logger.warning("Selected albums not found"); + return true; + } + + return false; + } + + Future _forceAlbumsUpdate() async { + await _loadAndRenderAlbums(); + await updateAlbumsChanged(false); + } + + Future _syncExistingAlbums() async { + final homeWidgetCount = await countHomeWidgets(); + if (homeWidgetCount == 0) { + _logger.warning("No active home widgets found"); + return; + } + + await _refreshWidget(message: "Refreshing from existing album set"); + } + + Future _isWidgetEmpty() async { + final totalAlbums = await _getTotalAlbums(); + return totalAlbums == 0 || totalAlbums == null; + } + + Future _shouldForceFetchAlbums(bool isWidgetEmpty) async { + // Check if albums changed flag is set + final albumsChanged = _prefs.getBool(ALBUMS_CHANGED_KEY); + if (albumsChanged == true) { + return true; + } + + // Check if we have any albums selected + final selectedAlbumIds = await _getEffectiveSelectedAlbumIds(); + if (selectedAlbumIds.isEmpty) { + _logger.warning("No albums selected"); + return false; + } + + // Check if hash has changed + final currentHash = _calculateHash(selectedAlbumIds); + final lastHash = getAlbumsLastHash(); + + if (currentHash == lastHash) { + final saveStatus = getAlbumsStatus(); + + switch (saveStatus) { + case WidgetStatus.syncedPartially: + return await countHomeWidgets() > 0; + case WidgetStatus.syncedEmpty: + case WidgetStatus.syncedAll: + return false; + default: + } + } + + return true; + } + + Future> _getEffectiveSelectedAlbumIds() async { + final selectedAlbumIds = getSelectedAlbumIds(); + + // If no albums selected, use favorites as default + if (selectedAlbumIds == null || selectedAlbumIds.isEmpty) { + final favoriteId = + await FavoritesService.instance.getFavoriteCollectionID(); + if (favoriteId != null) { + return [favoriteId]; + } + } + + return selectedAlbumIds ?? []; + } + + Future _getTotalAlbums() async { + return HomeWidgetService.instance.getData(TOTAL_ALBUMS_KEY); + } + + Future _setTotalAlbums(int? total) async { + await HomeWidgetService.instance.setData(TOTAL_ALBUMS_KEY, total); + } + + Future _refreshWidget({String? message}) async { + await HomeWidgetService.instance.updateWidget( + androidClass: ANDROID_CLASS_NAME, + iOSClass: IOS_CLASS_NAME, + ); + + if (flagService.internalUser) { + await Fluttertoast.showToast( + msg: "[i][al] ${message ?? "AlbumsHomeWidget updated"}", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: Colors.black, + textColor: Colors.white, + fontSize: 16.0, + ); + } + + _logger.info("Home Widget updated: ${message ?? "standard update"}"); + } + + Future)>> _getAlbumsWithFiles() async { + final selectedAlbumIds = await _getEffectiveSelectedAlbumIds(); + final albumsWithFiles = )>{}; + + for (final albumId in selectedAlbumIds) { + final collection = CollectionsService.instance.getCollectionByID(albumId); + if (collection != null) { + final files = + await FilesDB.instance.getAllFilesCollection(collection.id); + if (files.isNotEmpty) { + albumsWithFiles[collection.id] = + (collection.decryptedName ?? "Album", files); + } + } + } + + if (albumsWithFiles.isEmpty) { + _logger.warning("No albums with files found"); + } + + return albumsWithFiles; + } + + Future _loadAndRenderAlbums() async { + final albumsWithFiles = await _getAlbumsWithFiles(); + + if (albumsWithFiles.isEmpty) { + _logger.warning("No files found for any albums, clearing widget"); + await clearWidget(); + return; + } + + final currentTotal = await _getTotalAlbums(); + _logger.info("Current total albums in widget: $currentTotal"); + + final bool isWidgetPresent = await countHomeWidgets() > 0; + + final limit = isWidgetPresent ? MAX_ALBUMS_LIMIT : 5; + final maxAttempts = limit * 10; + + int renderedCount = 0; + int attemptsCount = 0; + + await updateAlbumsStatus(WidgetStatus.notSynced); + + final albumsWithFilesLength = albumsWithFiles.length; + final albumsWithFilesEntries = albumsWithFiles.entries.toList(); + final random = Random(); + + while (renderedCount < limit && attemptsCount < maxAttempts) { + final randomEntry = + albumsWithFilesEntries[random.nextInt(albumsWithFilesLength)]; + + if (randomEntry.value.$2.isEmpty) continue; + + final randomAlbumFile = randomEntry.value.$2.elementAt( + random.nextInt(randomEntry.value.$2.length), + ); + final albumId = randomEntry.key; + final albumName = randomEntry.value.$1; + + final renderResult = await HomeWidgetService.instance + .renderFile( + randomAlbumFile, + "albums_widget_$renderedCount", + albumName, + albumId.toString(), + ) + .catchError((e, stackTrace) { + _logger.severe("Error rendering widget", e, stackTrace); + return null; + }); + + if (renderResult != null) { + // Check for blockers again before continuing + if (await _hasAnyBlockers()) { + await clearWidget(); + return; + } + + await _setTotalAlbums(renderedCount); + + // Show update toast after first item is rendered + if (renderedCount == 1) { + await _refreshWidget( + message: "First album fetched, updating widget", + ); + await updateAlbumsStatus(WidgetStatus.syncedPartially); + } + + renderedCount++; + } + + attemptsCount++; + } + + if (attemptsCount >= maxAttempts) { + _logger.warning( + "Hit max attempts $maxAttempts. Only rendered $renderedCount of limit $limit.", + ); + } + + // Update the hash to track changes + final selectedAlbumIds = await _getEffectiveSelectedAlbumIds(); + final hash = _calculateHash(selectedAlbumIds); + await setAlbumsLastHash(hash); + + if (renderedCount == 0) { + return; + } + + if (isWidgetPresent) { + await updateAlbumsStatus(WidgetStatus.syncedAll); + } + + await _refreshWidget( + message: "Switched to next albums set, total: $renderedCount", + ); + } +} diff --git a/mobile/lib/services/collections_service.dart b/mobile/lib/services/collections_service.dart index 301655490a..afdb777ff5 100644 --- a/mobile/lib/services/collections_service.dart +++ b/mobile/lib/services/collections_service.dart @@ -398,7 +398,10 @@ class CollectionsService { .toList(); } - SharedCollections getSharedCollections() { + Future getSharedCollections() async { + final AlbumSortKey sortKey = localSettings.albumSortKey(); + final AlbumSortDirection sortDirection = localSettings.albumSortDirection(); + final List outgoing = []; final List incoming = []; final List quickLinks = []; @@ -415,17 +418,65 @@ class CollectionsService { incoming.add(c); } } - incoming.sort((first, second) { - return second.updationTime.compareTo(first.updationTime); - }); - outgoing.sort((first, second) { - return second.updationTime.compareTo(first.updationTime); - }); + + late Map collectionIDToNewestPhotoTime; + if (sortKey == AlbumSortKey.newestPhoto) { + collectionIDToNewestPhotoTime = + await CollectionsService.instance.getCollectionIDToNewestFileTime(); + } + + incoming.sort( + (first, second) { + int comparison; + if (sortKey == AlbumSortKey.albumName) { + comparison = compareAsciiLowerCaseNatural( + first.displayName, + second.displayName, + ); + } else if (sortKey == AlbumSortKey.newestPhoto) { + comparison = + (collectionIDToNewestPhotoTime[second.id] ?? -1 * intMaxValue) + .compareTo( + collectionIDToNewestPhotoTime[first.id] ?? -1 * intMaxValue, + ); + } else { + comparison = second.updationTime.compareTo(first.updationTime); + } + return sortDirection == AlbumSortDirection.ascending + ? comparison + : -comparison; + }, + ); + + outgoing.sort( + (first, second) { + int comparison; + if (sortKey == AlbumSortKey.albumName) { + comparison = compareAsciiLowerCaseNatural( + first.displayName, + second.displayName, + ); + } else if (sortKey == AlbumSortKey.newestPhoto) { + comparison = + (collectionIDToNewestPhotoTime[second.id] ?? -1 * intMaxValue) + .compareTo( + collectionIDToNewestPhotoTime[first.id] ?? -1 * intMaxValue, + ); + } else { + comparison = second.updationTime.compareTo(first.updationTime); + } + return sortDirection == AlbumSortDirection.ascending + ? comparison + : -comparison; + }, + ); + return SharedCollections(outgoing, incoming, quickLinks); } Future> getCollectionForOnEnteSection() async { final AlbumSortKey sortKey = localSettings.albumSortKey(); + final AlbumSortDirection sortDirection = localSettings.albumSortDirection(); final List collections = CollectionsService.instance.getCollectionsForUI(); final bool hasFavorites = FavoritesService.instance.hasFavorites(); @@ -436,19 +487,24 @@ class CollectionsService { } collections.sort( (first, second) { + int comparison; if (sortKey == AlbumSortKey.albumName) { - return compareAsciiLowerCaseNatural( + comparison = compareAsciiLowerCaseNatural( first.displayName, second.displayName, ); } else if (sortKey == AlbumSortKey.newestPhoto) { - return (collectionIDToNewestPhotoTime[second.id] ?? -1 * intMaxValue) - .compareTo( + comparison = + (collectionIDToNewestPhotoTime[second.id] ?? -1 * intMaxValue) + .compareTo( collectionIDToNewestPhotoTime[first.id] ?? -1 * intMaxValue, ); } else { - return second.updationTime.compareTo(first.updationTime); + comparison = second.updationTime.compareTo(first.updationTime); } + return sortDirection == AlbumSortDirection.ascending + ? comparison + : -comparison; }, ); final List favorites = []; @@ -1204,7 +1260,7 @@ class CollectionsService { return null; } - /// Is a public link opened in the app + /// Is a public link opened in the app via deeplink bool isSharedPublicLink(int collectionID) { return _cachedPublicCollectionID.contains(collectionID); } diff --git a/mobile/lib/services/favorites_service.dart b/mobile/lib/services/favorites_service.dart index 68cdc59944..9b8d9d6b59 100644 --- a/mobile/lib/services/favorites_service.dart +++ b/mobile/lib/services/favorites_service.dart @@ -60,7 +60,7 @@ class FavoritesService { } Future _warmUpCache() async { - final favCollection = await _getFavoritesCollection(); + final favCollection = await getFavoritesCollection(); if (favCollection != null) { Set uploadedIDs; Map fileHashes; @@ -103,7 +103,7 @@ class FavoritesService { } Future isFavorite(EnteFile file) async { - final collection = await _getFavoritesCollection(); + final collection = await getFavoritesCollection(); if (collection == null || file.uploadedFileID == null) { return false; } @@ -176,7 +176,7 @@ class FavoritesService { if (favFlag) { await _collectionsService.addOrCopyToCollection(collectionID, files); } else { - final Collection? favCollection = await _getFavoritesCollection(); + final Collection? favCollection = await getFavoritesCollection(); await _collectionActions.moveFilesFromCurrentCollection( context, favCollection!, @@ -194,7 +194,7 @@ class FavoritesService { if (inUploadID == null) { // Do nothing, ignore } else { - final Collection? favCollection = await _getFavoritesCollection(); + final Collection? favCollection = await getFavoritesCollection(); // The file might be part of another collection. For unfav, we need to // move file from the fav collection to the . if (file.ownerID != _config.getUserID() && @@ -225,7 +225,7 @@ class FavoritesService { _updateFavoriteFilesCache([file], favFlag: false); } - Future _getFavoritesCollection() async { + Future getFavoritesCollection() async { if (_cachedFavoritesCollectionID == null) { final collections = _collectionsService.getActiveCollections(); for (final collection in collections) { @@ -241,7 +241,7 @@ class FavoritesService { } Future getFavoriteCollectionID() async { - final collection = await _getFavoritesCollection(); + final collection = await getFavoritesCollection(); return collection?.id; } diff --git a/mobile/lib/services/filedata/filedata_service.dart b/mobile/lib/services/filedata/filedata_service.dart index dd01fa36e4..5407011313 100644 --- a/mobile/lib/services/filedata/filedata_service.dart +++ b/mobile/lib/services/filedata/filedata_service.dart @@ -1,9 +1,9 @@ import "dart:async"; import "package:computer/computer.dart"; +import "package:dio/dio.dart"; import "package:flutter/foundation.dart" show Uint8List; import "package:logging/logging.dart"; -import "package:photos/core/network/network.dart"; import "package:photos/db/files_db.dart"; import "package:photos/db/ml/db.dart"; import "package:photos/db/ml/filedata.dart"; @@ -16,26 +16,23 @@ import "package:photos/utils/gzip.dart"; import "package:shared_preferences/shared_preferences.dart"; class FileDataService { - FileDataService._privateConstructor(); - static final Computer _computer = Computer.shared(); - static final FileDataService instance = FileDataService._privateConstructor(); final _logger = Logger("FileDataService"); - final _dio = NetworkClient.instance.enteDio; - late final SharedPreferences _prefs; - Map? previewIds; + final Dio _dio; + final SharedPreferences _prefs; + late Map previewIds; - void init(SharedPreferences prefs) { - _prefs = prefs; + FileDataService(this._prefs, this._dio) { + _logger.info("FileDataService constructor called"); + previewIds = {}; } /// Used to not sync preview ids everytime a chunking and preview /// upload is successful, instead update the local copy of those /// preview ids void appendPreview(int id, String objectId, int objectSize) { - if (previewIds?.containsKey(id) ?? false) return; - previewIds ??= {}; - previewIds?[id] = PreviewInfo( + if (previewIds.containsKey(id)) return; + previewIds[id] = PreviewInfo( objectId: objectId, objectSize: objectSize, ); @@ -152,6 +149,9 @@ class FileDataService { previewIds = await MLDataDB.instance.getFileIDsVidPreview(); } while (hasMoreData); } catch (e) { + if (previewIds.isEmpty) { + previewIds = await MLDataDB.instance.getFileIDsVidPreview(); + } _logger.severe("Failed to syncDiff", e); rethrow; } diff --git a/mobile/lib/services/filter/db_filters.dart b/mobile/lib/services/filter/db_filters.dart index 5329b4825c..a4ec0bce9d 100644 --- a/mobile/lib/services/filter/db_filters.dart +++ b/mobile/lib/services/filter/db_filters.dart @@ -4,6 +4,7 @@ import "package:photos/services/filter/collection_ignore.dart"; import "package:photos/services/filter/dedupe_by_upload_id.dart"; import "package:photos/services/filter/filter.dart"; import "package:photos/services/filter/only_uploaded_files_filter.dart"; +import "package:photos/services/filter/shared.dart"; import "package:photos/services/filter/upload_ignore.dart"; import "package:photos/services/ignored_files_service.dart"; @@ -18,12 +19,16 @@ class DBFilterOptions { bool ignoreSavedFiles; bool onlyUploadedFiles; + // If true, files owned by other users or uploaded by other users will be ignored. + bool ignoreSharedItems = false; + DBFilterOptions({ this.ignoredCollectionIDs, this.hideIgnoredForUpload = false, this.dedupeUploadID = true, this.ignoreSavedFiles = false, this.onlyUploadedFiles = false, + this.ignoreSharedItems = false, }); static DBFilterOptions dedupeOption = DBFilterOptions( @@ -39,6 +44,9 @@ Future> applyDBFilters( return files; } final List filters = []; + if (options.ignoreSharedItems) { + filters.add(SkipSharedFileFilter()); + } if (options.hideIgnoredForUpload) { final Map idToReasonMap = await IgnoredFilesService.instance.idToIgnoreReasonMap; diff --git a/mobile/lib/services/filter/shared.dart b/mobile/lib/services/filter/shared.dart new file mode 100644 index 0000000000..823573ff88 --- /dev/null +++ b/mobile/lib/services/filter/shared.dart @@ -0,0 +1,10 @@ +import "package:photos/models/file/extensions/file_props.dart"; +import "package:photos/models/file/file.dart"; +import "package:photos/services/filter/filter.dart"; + +class SkipSharedFileFilter extends Filter { + @override + bool filter(EnteFile file) { + return file.uploaderName == null && file.isOwner; + } +} diff --git a/mobile/lib/services/home_widget_service.dart b/mobile/lib/services/home_widget_service.dart index 8e11dfa574..aac3853aad 100644 --- a/mobile/lib/services/home_widget_service.dart +++ b/mobile/lib/services/home_widget_service.dart @@ -1,29 +1,62 @@ -import "dart:convert"; -import "dart:io"; +import 'dart:convert'; +import 'dart:io'; -import "package:flutter/material.dart"; +import 'package:flutter/material.dart'; import 'package:home_widget/home_widget.dart' as hw; -import "package:logging/logging.dart"; -import "package:path_provider/path_provider.dart"; -import "package:path_provider_foundation/path_provider_foundation.dart"; -import "package:photos/core/constants.dart"; -import "package:photos/models/file/file.dart"; -import "package:photos/services/memory_home_widget_service.dart"; -import "package:photos/services/smart_memories_service.dart"; -import "package:photos/utils/thumbnail_util.dart"; -import "package:shared_preferences/shared_preferences.dart"; +import 'package:home_widget/home_widget.dart'; +import 'package:logging/logging.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path_provider_foundation/path_provider_foundation.dart'; +import 'package:photos/core/constants.dart'; +import 'package:photos/models/file/file.dart'; +import 'package:photos/services/album_home_widget_service.dart'; +import 'package:photos/services/memory_home_widget_service.dart'; +import 'package:photos/services/people_home_widget_service.dart'; +import 'package:photos/services/smart_memories_service.dart'; +import 'package:photos/utils/thumbnail_util.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +enum WidgetStatus { + notSynced, + syncedPartially, + syncedEmpty, + syncedAll, +} + +/// Service to manage home screen widgets across the application +/// Handles widget initialization, updates, and interaction with platform-specific widget APIs class HomeWidgetService { - final Logger _logger = Logger((HomeWidgetService).toString()); + // Constants + static const double THUMBNAIL_SIZE = 512.0; + static const String WIDGET_DIRECTORY = 'home_widget'; - HomeWidgetService._privateConstructor(); + // URI schemes for different widget types + static const String MEMORY_WIDGET_SCHEME = 'memorywidget'; + static const String PEOPLE_WIDGET_SCHEME = 'peoplewidget'; + static const String ALBUM_WIDGET_SCHEME = 'albumwidget'; + + // Query parameter keys + static const String GENERATED_ID_PARAM = 'generatedId'; + static const String MAIN_KEY_PARAM = 'mainKey'; + + // Widget data keys + static const String DATA_SUFFIX = '_data'; static final HomeWidgetService instance = HomeWidgetService._privateConstructor(); + HomeWidgetService._privateConstructor(); - init(SharedPreferences prefs) { - setAppGroupID(iOSGroupID); + final Logger _logger = Logger((HomeWidgetService).toString()); + + void init(SharedPreferences prefs) { + setAppGroupID(iOSGroupIDMemory); + _initializeWidgetServices(prefs); + } + + void _initializeWidgetServices(SharedPreferences prefs) { MemoryHomeWidgetService.instance.init(prefs); + PeopleHomeWidgetService.instance.init(prefs); + AlbumHomeWidgetService.instance.init(prefs); } void setAppGroupID(String id) { @@ -31,7 +64,9 @@ class HomeWidgetService { } Future initHomeWidget() async { - await MemoryHomeWidgetService.instance.initMemoryHW(null); + await MemoryHomeWidgetService.instance.initMemoryHomeWidget(null); + await PeopleHomeWidgetService.instance.initHomeWidget(null); + await AlbumHomeWidgetService.instance.initHomeWidget(null); } Future updateWidget({ @@ -46,114 +81,202 @@ class HomeWidgetService { ); } - Future getData(String key) async => - await hw.HomeWidget.getWidgetData(key); + Future getData(String key) async { + return hw.HomeWidget.getWidgetData(key); + } - Future setData(String key, T? data) async => - await hw.HomeWidget.saveWidgetData(key, data); + Future setData(String key, T? data) async { + return hw.HomeWidget.saveWidgetData(key, data); + } Future renderFile( - EnteFile randomFile, + EnteFile file, String key, String title, + String? mainKey, ) async { - const size = 512.0; - - final result = await _captureFile(randomFile, key, title); + final result = await _captureFile(file, key, title, mainKey); if (!result) { - _logger.warning("can't capture file ${randomFile.displayName}"); + _logger.warning("Failed to capture file ${file.displayName}"); return null; } - return const Size(size, size); + return const Size(THUMBNAIL_SIZE, THUMBNAIL_SIZE); } - Future countHomeWidgets() async { - return (await hw.HomeWidget.getInstalledWidgets()).length; + Future countHomeWidgets( + String androidClass, + String iOSClass, + ) async { + final installedWidgets = await getInstalledWidgets(); + final relevantWidgets = installedWidgets + .where( + (widget) => + (widget.androidClassName?.contains(androidClass) ?? false) || + widget.iOSKind == iOSClass, + ) + .toList(); + + return relevantWidgets.length; + } + + Future> getInstalledWidgets() async { + return await hw.HomeWidget.getInstalledWidgets(); } Future _captureFile( - EnteFile ogFile, + EnteFile file, String key, String title, + String? mainKey, ) async { try { - final thumbnail = await getThumbnail(ogFile); - - late final String? directory; - - // coverage:ignore-start - if (Platform.isIOS) { - final PathProviderFoundation provider = PathProviderFoundation(); - directory = await provider.getContainerPath( - appGroupIdentifier: iOSGroupID, - ); - } else { - directory = (await getApplicationSupportDirectory()).path; + // Get thumbnail data + final thumbnail = await getThumbnail(file); + if (thumbnail == null) { + _logger.warning("Failed to get thumbnail for file ${file.displayName}"); + return false; } - final String path = '$directory/home_widget/$key.png'; - final File file = File(path); - if (!await file.exists()) { - await file.create(recursive: true); + // Get appropriate directory for widget assets + final String widgetDirectory = await _getWidgetStorageDirectory(); + + // Save thumbnail to file + final String thumbnailPath = + '$widgetDirectory/$WIDGET_DIRECTORY/$key.png'; + final File thumbnailFile = File(thumbnailPath); + + if (!await thumbnailFile.exists()) { + await thumbnailFile.create(recursive: true); } - await file.writeAsBytes(thumbnail!); - await setData(key, path); + await thumbnailFile.writeAsBytes(thumbnail); + await setData(key, thumbnailPath); + // Format date for display final subText = await SmartMemoriesService.getDateFormattedLocale( - creationTime: ogFile.creationTime!, + creationTime: file.creationTime!, ); - final data = { + // Create metadata + final Map metadata = { "title": title, "subText": subText, - "generatedId": ogFile.generatedID!, + "generatedId": file.generatedID!, + if (mainKey != null) "mainKey": mainKey, }; - if (Platform.isIOS) { - await hw.HomeWidget.saveWidgetData>( - key + "_data", - data, - ); - } else { - await hw.HomeWidget.saveWidgetData( - key + "_data", - jsonEncode(data), - ); - } - } catch (_, __) { - _logger.severe("Failed to save the capture", _, __); + + // Save metadata in platform-specific format + await _saveWidgetMetadata(key, metadata); + + return true; + } catch (error, stackTrace) { + _logger.severe("Failed to save the thumbnail", error, stackTrace); return false; } - return true; + } + + Future _saveWidgetMetadata( + String key, + Map metadata, + ) async { + final String dataKey = key + DATA_SUFFIX; + + if (Platform.isIOS) { + // iOS can store the map directly + await hw.HomeWidget.saveWidgetData>( + dataKey, + metadata, + ); + } else { + // Android needs the data as a JSON string + await hw.HomeWidget.saveWidgetData( + dataKey, + jsonEncode(metadata), + ); + } + } + + Future _getWidgetStorageDirectory() async { + if (Platform.isIOS) { + final PathProviderFoundation provider = PathProviderFoundation(); + return (await provider.getContainerPath( + appGroupIdentifier: iOSGroupIDMemory, + ))!; + } else { + return (await getApplicationSupportDirectory()).path; + } } Future clearWidget(bool autoLogout) async { if (autoLogout) { - setAppGroupID(iOSGroupID); + setAppGroupID(iOSGroupIDMemory); } - await MemoryHomeWidgetService.instance.clearWidget(); + + await Future.wait([ + MemoryHomeWidgetService.instance.clearWidget(), + PeopleHomeWidgetService.instance.clearWidget(), + AlbumHomeWidgetService.instance.clearWidget(), + ]); } + /// Handle app launch from a widget Future onLaunchFromWidget(Uri? uri, BuildContext context) async { if (uri == null) { - _logger.warning("onLaunchFromWidget: uri is null"); + _logger.warning("Widget launch failed: URI is null"); return; } - final generatedId = int.tryParse(uri.queryParameters["generatedId"] ?? ""); - + final generatedId = + int.tryParse(uri.queryParameters[GENERATED_ID_PARAM] ?? ""); if (generatedId == null) { - _logger.warning("onLaunchFromWidget: generatedId is null"); + _logger.warning("Widget launch failed: Invalid or missing generated ID"); return; } - if (uri.scheme == "memorywidget") { - _logger.info("onLaunchFromWidget: redirecting to memory widget"); - await MemoryHomeWidgetService.instance.onLaunchFromWidget( - generatedId, - context, - ); + // Route to appropriate handler based on widget scheme + switch (uri.scheme) { + case MEMORY_WIDGET_SCHEME: + _logger.info("Launching app from memory widget"); + await MemoryHomeWidgetService.instance.onLaunchFromWidget( + generatedId, + context, + ); + break; + + case PEOPLE_WIDGET_SCHEME: + _logger.info("Launching app from people widget"); + final personId = uri.queryParameters[MAIN_KEY_PARAM] ?? ""; + await PeopleHomeWidgetService.instance.onLaunchFromWidget( + generatedId, + personId, + context, + ); + break; + + case ALBUM_WIDGET_SCHEME: + _logger.info("Launching app from album widget"); + final collectionId = + int.tryParse(uri.queryParameters[MAIN_KEY_PARAM] ?? ""); + if (collectionId == null) { + _logger.warning( + "Album widget launch failed: Invalid or missing collection ID", + ); + return; + } + + await AlbumHomeWidgetService.instance.onLaunchFromWidget( + generatedId, + collectionId, + context, + ); + break; + + default: + _logger.warning( + "Widget launch failed: Unknown widget scheme '${uri.scheme}'", + ); + break; } } } diff --git a/mobile/lib/services/isolate_functions.dart b/mobile/lib/services/isolate_functions.dart index 929790c588..ae4e1e8851 100644 --- a/mobile/lib/services/isolate_functions.dart +++ b/mobile/lib/services/isolate_functions.dart @@ -1,4 +1,3 @@ -import "dart:io" show File; import 'dart:typed_data' show Uint8List; import "package:ml_linalg/linalg.dart"; @@ -98,12 +97,11 @@ Future isolateFunction( /// MLComputer case IsolateOperation.generateFaceThumbnails: final imagePath = args['imagePath'] as String; - final Uint8List imageData = await File(imagePath).readAsBytes(); final faceBoxesJson = args['faceBoxesList'] as List>; final List faceBoxes = faceBoxesJson.map((json) => FaceBox.fromJson(json)).toList(); final List results = await generateFaceThumbnailsUsingCanvas( - imageData, + imagePath, faceBoxes, ); return List.from(results); diff --git a/mobile/lib/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart b/mobile/lib/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart index ce29ccc269..b97573fa60 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart @@ -8,6 +8,9 @@ const kLaplacianVerySoftThreshold = 200; /// Default blur value const kLapacianDefault = 10000.0; +/// The minimum score for a face to be shown as detected in the UI +const kMinimumFaceShowScore = 0.70; + /// The minimum score for a face to be considered a high quality face for clustering and person detection const kMinimumQualityFaceScore = 0.80; const kMediumQualityFaceScore = 0.85; diff --git a/mobile/lib/services/machine_learning/face_ml/person/person_service.dart b/mobile/lib/services/machine_learning/face_ml/person/person_service.dart index daa3ddc0ff..7c96a67dea 100644 --- a/mobile/lib/services/machine_learning/face_ml/person/person_service.dart +++ b/mobile/lib/services/machine_learning/face_ml/person/person_service.dart @@ -4,7 +4,6 @@ import "dart:developer"; import "package:flutter/foundation.dart"; import "package:logging/logging.dart"; import "package:photos/core/event_bus.dart"; -import "package:photos/db/files_db.dart"; import "package:photos/db/ml/db.dart"; import "package:photos/events/people_changed_event.dart"; import "package:photos/extensions/stop_watch.dart"; @@ -14,8 +13,7 @@ import 'package:photos/models/ml/face/face.dart'; import "package:photos/models/ml/face/person.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/entity_service.dart"; -import "package:photos/services/machine_learning/ml_result.dart"; -import "package:photos/services/search_service.dart"; +import "package:photos/utils/face/face_thumbnail_cache.dart"; import "package:shared_preferences/shared_preferences.dart"; class PersonService { @@ -38,6 +36,8 @@ class PersonService { return _instance!; } + static bool get isInitialized => _instance != null; + late Logger logger = Logger("PersonService"); static Future init( @@ -443,6 +443,7 @@ class PersonService { data: person.data.copyWith(avatarFaceId: face.faceID), ); await updatePerson(updatedPerson); + await putFaceIdCachedForPersonOrCluster(p.remoteID, face.faceID); return updatedPerson; } @@ -484,49 +485,4 @@ class PersonService { rethrow; } } - - Future getThumbnailFileOfPerson( - PersonEntity person, - ) async { - try { - if (person.data.hasAvatar()) { - final avatarFileID = tryGetFileIdFromFaceId(person.data.avatarFaceID!); - if (avatarFileID != null) { - final file = (await FilesDB.instance - .getFileIDToFileFromIDs([avatarFileID]))[avatarFileID]; - if (file != null) { - return file; - } else { - logger.severe("Avatar File not found for face ${person.data}"); - } - } - } - - final clustersToFiles = - await SearchService.instance.getClusterFilesForPersonID( - person.remoteID, - ); - - EnteFile? resultFile; - // iterate over all clusters and get the first file - for (final clusterFiles in clustersToFiles.values) { - for (final file in clusterFiles) { - resultFile ??= file; - if (resultFile.creationTime! < file.creationTime!) { - resultFile = file; - } - } - } - if (resultFile == null) { - logger.warning( - "Person ${kDebugMode ? person.data.name : person.remoteID} has no files", - ); - return null; - } - return resultFile; - } catch (e, s) { - logger.severe("Error in getThumbnailFileOfPerson", e, s); - return null; - } - } } diff --git a/mobile/lib/services/machine_learning/ml_indexing_isolate.dart b/mobile/lib/services/machine_learning/ml_indexing_isolate.dart index 52da789eb7..a9553b3fa7 100644 --- a/mobile/lib/services/machine_learning/ml_indexing_isolate.dart +++ b/mobile/lib/services/machine_learning/ml_indexing_isolate.dart @@ -28,6 +28,9 @@ class MLIndexingIsolate extends SuperIsolate { @override bool get shouldAutomaticDispose => true; + int _loadedModelsCount = 0; + int _deloadedModelsCount = 0; + final _initModelLock = Lock(); final _downloadModelLock = Lock(); @@ -144,6 +147,10 @@ class MLIndexingIsolate extends SuperIsolate { _logger.info( 'Loading models. faces: $shouldLoadFaces, clip: $shouldLoadClip', ); + _loadedModelsCount++; + _logger.info( + "Loaded models count: $_loadedModelsCount, deloaded models count: $_deloadedModelsCount", + ); await MLIndexingIsolate.instance ._loadModels(loadFaces: shouldLoadFaces, loadClip: shouldLoadClip); _logger.info('Models loaded'); @@ -249,6 +256,11 @@ class MLIndexingIsolate extends SuperIsolate { } if (modelNames.isEmpty) return; try { + _logger.info("Releasing models $modelNames"); + _deloadedModelsCount++; + _logger.info( + "Loaded models count: $_loadedModelsCount, deloaded models count: $_deloadedModelsCount", + ); await runInIsolate(IsolateOperation.releaseIndexingModels, { "modelNames": modelNames, "modelAddresses": modelAddresses, diff --git a/mobile/lib/services/machine_learning/ml_service.dart b/mobile/lib/services/machine_learning/ml_service.dart index 0961b50e4c..1152b1353c 100644 --- a/mobile/lib/services/machine_learning/ml_service.dart +++ b/mobile/lib/services/machine_learning/ml_service.dart @@ -13,7 +13,6 @@ import "package:photos/events/people_changed_event.dart"; import "package:photos/models/ml/face/face.dart"; import "package:photos/models/ml/ml_versions.dart"; import "package:photos/service_locator.dart"; -import "package:photos/services/filedata/filedata_service.dart"; import "package:photos/services/filedata/model/file_data.dart"; import 'package:photos/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart'; import "package:photos/services/machine_learning/face_ml/face_clustering/face_db_info_for_clustering.dart"; @@ -51,6 +50,9 @@ class MLService { bool _isRunningML = false; bool _shouldPauseIndexingAndClustering = false; + bool get isRunningML => + _isRunningML || memoriesCacheService.isUpdatingMemories; + static const _kForceClusteringFaceCount = 8000; late final mlDataDB = MLDataDB.instance; @@ -116,7 +118,7 @@ class MLService { } Future sync() async { - await FileDataService.instance.syncFDStatus(); + await fileDataService.syncFDStatus(); await faceRecognitionService.syncPersonFeedback(); } @@ -494,7 +496,7 @@ class MLService { ); } // Storing results on remote - await FileDataService.instance.putFileData( + await fileDataService.putFileData( instruction.file, dataEntity, ); diff --git a/mobile/lib/services/memories_cache_service.dart b/mobile/lib/services/memories_cache_service.dart index ed2bd9d108..17fd486f33 100644 --- a/mobile/lib/services/memories_cache_service.dart +++ b/mobile/lib/services/memories_cache_service.dart @@ -18,6 +18,8 @@ import "package:photos/models/memories/memory.dart"; import "package:photos/models/memories/smart_memory.dart"; import "package:photos/models/memories/smart_memory_constants.dart"; import "package:photos/service_locator.dart"; +import "package:photos/services/language_service.dart"; +import "package:photos/services/notification_service.dart"; import "package:photos/services/search_service.dart"; import "package:photos/ui/home/memories/full_screen_memory.dart"; import "package:photos/utils/navigation_util.dart"; @@ -41,6 +43,9 @@ class MemoriesCacheService { List? _cachedMemories; bool _shouldUpdate = false; + bool _isUpdatingMemories = false; + bool get isUpdatingMemories => _isUpdatingMemories; + final _memoriesUpdateLock = Lock(); MemoriesCacheService(this._prefs) { @@ -90,6 +95,17 @@ class MemoriesCacheService { Bus.instance.fire(MemoriesSettingChanged()); } + Future toggleOnThisDayNotifications() async { + final oldValue = localSettings.isOnThisDayNotificationsEnabled; + await localSettings.setOnThisDayNotificationsEnabled(!oldValue); + _logger.info("Turning onThisDayNotifications ${oldValue ? "off" : "on"}"); + if (oldValue) { + await _clearAllScheduledOnThisDayNotifications(); + } else { + queueUpdateCache(); + } + } + bool get enableSmartMemories => flagService.hasGrantedMLConsent && localSettings.isMLLocalIndexingEnabled && @@ -173,6 +189,7 @@ class MemoriesCacheService { _logger.info( "Updating memories cache (shouldUpdate: $_shouldUpdate, forced: $forced)", ); + _isUpdatingMemories = true; try { final EnteWatch? w = kDebugMode ? EnteWatch("MemoriesCacheService") : null; @@ -185,7 +202,7 @@ class MemoriesCacheService { final now = DateTime.now(); final next = now.add(kMemoriesUpdateFrequency); final nowResult = - await smartMemoriesService.calcMemories(now, newCache); + await smartMemoriesService.calcSmartMemories(now, newCache); if (nowResult.isEmpty) { _cachedMemories = []; _logger.warning( @@ -194,7 +211,7 @@ class MemoriesCacheService { return; } final nextResult = - await smartMemoriesService.calcMemories(next, newCache); + await smartMemoriesService.calcSmartMemories(next, newCache); w?.log("calculated new memories"); for (final nowMemory in nowResult.memories) { newCache.toShowMemories @@ -213,6 +230,9 @@ class MemoriesCacheService { _cachedMemories = nowResult.memories .where((memory) => memory.shouldShowNow()) .toList(); + await _scheduleOnThisDayNotifications( + [...nowResult.memories, ...nextResult.memories], + ); locationService.baseLocations = nowResult.baseLocations; await file.writeAsBytes( MemoriesCache.encodeToJsonString(newCache).codeUnits, @@ -222,6 +242,8 @@ class MemoriesCacheService { w?.logAndReset('_cacheUpdated method done'); } catch (e, s) { _logger.info("Error updating memories cache", e, s); + } finally { + _isUpdatingMemories = false; } }); } @@ -233,6 +255,66 @@ class MemoriesCacheService { return newCache; } + Future _clearAllScheduledOnThisDayNotifications() async { + _logger.info('Clearing all scheduled On This Day notifications'); + await NotificationService.instance + .clearAllScheduledNotifications(containingPayload: "onThisDay"); + } + + Future _scheduleOnThisDayNotifications( + List allMemories, + ) async { + if (!localSettings.isOnThisDayNotificationsEnabled) { + _logger + .info("On this day notifications are disabled, skipping scheduling"); + return; + } + await _clearAllScheduledOnThisDayNotifications(); + final scheduledDates = {}; + for (final memory in allMemories) { + if (memory.type != MemoryType.onThisDay) { + continue; + } + final numberOfMemories = memory.memories.length; + if (numberOfMemories < 5) continue; + final firstDateToShow = + DateTime.fromMicrosecondsSinceEpoch(memory.firstDateToShow); + final scheduleTime = DateTime( + firstDateToShow.year, + firstDateToShow.month, + firstDateToShow.day, + 8, + ); + if (scheduleTime.isBefore(DateTime.now())) { + _logger.info( + "Skipping scheduling notification for memory ${memory.id} because the date is in the past (date: $scheduleTime)", + ); + continue; + } + if (scheduledDates.contains(scheduleTime)) { + _logger.info( + "Skipping scheduling notification for memory ${memory.id} because the date is already scheduled (date: $scheduleTime)", + ); + continue; + } + final s = await LanguageService.s; + await NotificationService.instance.scheduleNotification( + s.onThisDay, + s.lookBackOnYourMemories, + id: memory.id.hashCode, + channelID: "onThisDay", + channelName: s.onThisDay, + payload: memory.id, + dateTime: scheduleTime, + timeoutDurationAndroid: const Duration(hours: 16), + ); + scheduledDates.add(scheduleTime); + _logger.info( + "Scheduled notification for memory ${memory.id} on $scheduleTime", + ); + } + } + MemoriesCache _processOldCache(MemoriesCache? oldCache) { final List peopleShownLogs = []; final List clipShownLogs = []; @@ -313,6 +395,7 @@ class MemoriesCacheService { memory.title, memory.firstTimeToShow, memory.lastTimeToShow, + id: memory.id, ); if (smartMemory.memories.isNotEmpty) { memories.add(smartMemory); @@ -339,12 +422,42 @@ class MemoriesCacheService { Future _calculateRegularFillers() async { if (_cachedMemories == null) { - _cachedMemories = await smartMemoriesService.calcFillerResults(); + _cachedMemories = await smartMemoriesService.calcSimpleMemories(); Bus.instance.fire(MemoriesChangedEvent()); } return; } + Future> getMemoriesForWidget({ + required bool onThisDay, + required bool pastYears, + required bool smart, + }) async { + if (!onThisDay && !pastYears && !smart) { + _logger.info( + 'No memories requested, returning empty list', + ); + return []; + } + final allMemories = await getMemories(); + if (onThisDay && pastYears && smart) { + return allMemories; + } + final filteredMemories = []; + for (final memory in allMemories) { + if (!memory.shouldShowNow()) continue; + if (memory.type == MemoryType.onThisDay) { + if (!onThisDay) continue; + } else if (memory.type == MemoryType.filler) { + if (!pastYears) continue; + } else { + if (!smart) continue; + } + filteredMemories.add(memory); + } + return filteredMemories; + } + Future> getMemories() async { if (!showAnyMemories) { _logger.info('Showing memories is disabled in settings, showing none'); @@ -360,7 +473,12 @@ class MemoriesCacheService { } _cachedMemories = await _getMemoriesFromCache(); if (_cachedMemories == null || _cachedMemories!.isEmpty) { + _logger.warning( + "No memories found in cache, force updating cache. Possible severe caching issue", + ); await updateCache(forced: true); + } else { + _logger.info("Found memories in cache"); } if (_cachedMemories == null || _cachedMemories!.isEmpty) { _logger @@ -416,6 +534,36 @@ class MemoriesCacheService { ); } + Future goToOnThisDayMemory(BuildContext context) async { + final allMemories = await getMemories(); + if (allMemories.isEmpty) return; + int memoryIdx = 0; + bool found = false; + memoryLoop: + for (final memory in allMemories) { + if (memory.type == MemoryType.onThisDay) { + found = true; + break memoryLoop; + } + memoryIdx++; + } + if (!found) { + _logger.warning( + "Could not find onThisDay memory", + ); + return; + } + await routeToPage( + context, + FullScreenMemoryDataUpdater( + initialIndex: 0, + memories: allMemories[memoryIdx].memories, + child: FullScreenMemory(allMemories[memoryIdx].title, 0), + ), + forceCustomPageRoute: true, + ); + } + Future _readCacheFromDisk() async { _logger.info("Reading memories cache result from disk"); final file = File(await _getCachePath()); diff --git a/mobile/lib/services/memory_home_widget_service.dart b/mobile/lib/services/memory_home_widget_service.dart index 6b76bff295..c015dd0a69 100644 --- a/mobile/lib/services/memory_home_widget_service.dart +++ b/mobile/lib/services/memory_home_widget_service.dart @@ -1,164 +1,284 @@ -import "package:flutter/material.dart"; -import "package:fluttertoast/fluttertoast.dart"; -import "package:logging/logging.dart"; -import "package:photos/models/file/file.dart"; -import "package:photos/service_locator.dart"; -import "package:photos/services/home_widget_service.dart"; -import "package:photos/services/sync/local_sync_service.dart"; -import "package:shared_preferences/shared_preferences.dart"; -import "package:synchronized/synchronized.dart"; +import 'dart:math'; +import "package:collection/collection.dart"; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:logging/logging.dart'; +import 'package:photos/models/file/file.dart'; +import 'package:photos/models/memories/smart_memory.dart'; +import 'package:photos/service_locator.dart'; +import 'package:photos/services/home_widget_service.dart'; +import 'package:photos/services/sync/local_sync_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:synchronized/synchronized.dart'; class MemoryHomeWidgetService { - final Logger _logger = Logger((MemoryHomeWidgetService).toString()); - - MemoryHomeWidgetService._privateConstructor(); + // Constants + static const String SELECTED_LAST_YEAR_MEMORIES_KEY = + "selectedLastYearMemoriesHW"; + static const String SELECTED_ML_MEMORIES_KEY = "selectedMLMemoriesHW"; + static const String SELECTED_ON_THIS_DAY_MEMORIES_KEY = + "selectedOnThisDayMemoriesHW"; + static const String ANDROID_CLASS_NAME = "EnteMemoryWidgetProvider"; + static const String IOS_CLASS_NAME = "EnteMemoryWidget"; + static const String MEMORY_STATUS_KEY = "memoryStatusKey.widget"; + static const String MEMORY_CHANGED_KEY = "memoryChanged.widget"; + static const String TOTAL_MEMORIES_KEY = "totalMemories"; + static const int MAX_MEMORIES_LIMIT = 50; + // Singleton pattern static final MemoryHomeWidgetService instance = MemoryHomeWidgetService._privateConstructor(); + MemoryHomeWidgetService._privateConstructor(); + // Properties + final Logger _logger = Logger((MemoryHomeWidgetService).toString()); late final SharedPreferences _prefs; - final _memoryForceRefreshLock = Lock(); bool _hasSyncedMemory = false; - static const memoryChangedKey = "memoryChanged.widget"; - static const totalMemories = "totalMemories"; - - init(SharedPreferences prefs) { + // Initialization + void init(SharedPreferences prefs) { _prefs = prefs; } - Future _forceMemoryUpdate() async { - await _lockAndLoadMemories(); - await updateMemoryChanged(false); + // Preference getters and setters + Future getSelectedLastYearMemories() async { + return _prefs.getBool(SELECTED_LAST_YEAR_MEMORIES_KEY); } - Future _memorySync() async { - final homeWidgetCount = await HomeWidgetService.instance.countHomeWidgets(); - if (homeWidgetCount == 0) { - _logger.warning("no home widget active"); + Future setSelectedLastYearMemories(bool selectedMemories) async { + await _prefs.setBool(SELECTED_LAST_YEAR_MEMORIES_KEY, selectedMemories); + } + + Future getSelectedMLMemories() async { + return _prefs.getBool(SELECTED_ML_MEMORIES_KEY); + } + + Future setSelectedMLMemories(bool selectedMemories) async { + await _prefs.setBool(SELECTED_ML_MEMORIES_KEY, selectedMemories); + } + + Future getSelectedOnThisDayMemories() async { + return _prefs.getBool(SELECTED_ON_THIS_DAY_MEMORIES_KEY); + } + + Future setSelectedOnThisDayMemories(bool selectedMemories) async { + await _prefs.setBool(SELECTED_ON_THIS_DAY_MEMORIES_KEY, selectedMemories); + } + + // Public methods + Future initMemoryHomeWidget(bool? forceFetchNewMemories) async { + if (await _hasAnyBlockers()) { + await clearWidget(); return; } - await _updateWidget(text: "refreshing from same set"); + await _memoryForceRefreshLock.synchronized(() async { + if (await _hasAnyBlockers()) { + return; + } + + final isWidgetEmpty = await _isWidgetEmpty(); + forceFetchNewMemories ??= await _shouldForceFetchMemories(isWidgetEmpty); + + _logger.warning( + "Initializing memory widget: forceFetch: $forceFetchNewMemories, isEmpty: $isWidgetEmpty", + ); + + if (forceFetchNewMemories!) { + await _forceMemoryUpdate(); + } else if (!isWidgetEmpty) { + await _syncExistingMemories(); + } + }); } - Future hasAnyBlockers() async { + Future clearWidget() async { + final isWidgetEmpty = await _isWidgetEmpty(); + if (isWidgetEmpty) { + _logger.info("Widget already empty, nothing to clear"); + return; + } + + _logger.info("Clearing MemoryHomeWidget"); + await _setTotalMemories(null); + _hasSyncedMemory = false; + await updateMemoriesStatus(WidgetStatus.syncedEmpty); + await _refreshWidget(message: "MemoryHomeWidget cleared & updated"); + } + + Future updateMemoryChanged(bool value) async { + _logger.info("Updating memory changed flag to $value"); + await _prefs.setBool(MEMORY_CHANGED_KEY, value); + } + + WidgetStatus getMemoriesStatus() { + return WidgetStatus.values.firstWhereOrNull( + (v) => v.index == (_prefs.getInt(MEMORY_STATUS_KEY) ?? 0), + ) ?? + WidgetStatus.notSynced; + } + + Future updateMemoriesStatus(WidgetStatus value) async { + await _prefs.setInt(MEMORY_STATUS_KEY, value.index); + } + + Future checkPendingMemorySync({bool addDelay = true}) async { + if (addDelay) { + await Future.delayed(const Duration(seconds: 5)); + } + + final isWidgetEmpty = await _isWidgetEmpty(); + final shouldForceFetch = await _shouldForceFetchMemories(isWidgetEmpty); + + if (_hasSyncedMemory && !shouldForceFetch) { + _logger.info("Memories already synced, no action needed"); + return; + } + + await initMemoryHomeWidget(shouldForceFetch); + } + + Future memoryChanged() async { + await updateMemoryChanged(true); + + final cachedMemories = await _getMemoriesForWidget(); + final currentTotal = cachedMemories.length; + final existingTotal = await _getTotalMemories() ?? 0; + + if (existingTotal == currentTotal && existingTotal == 0) { + await updateMemoryChanged(false); + _logger.info("Memories empty, no update needed"); + return; + } + + _logger.info("Memories changed, updating widget"); + await initMemoryHomeWidget(true); + } + + Future countHomeWidgets() async { + return await HomeWidgetService.instance.countHomeWidgets( + ANDROID_CLASS_NAME, + IOS_CLASS_NAME, + ); + } + + Future onLaunchFromWidget(int generatedId, BuildContext context) async { + _hasSyncedMemory = true; + await _syncExistingMemories(); + + await memoriesCacheService.goToMemoryFromGeneratedFileID( + context, + generatedId, + ); + } + + // Private methods + Future _hasAnyBlockers() async { + // Check if first import is completed final hasCompletedFirstImport = LocalSyncService.instance.hasCompletedFirstImport(); if (!hasCompletedFirstImport) { - _logger.warning("first import not completed"); + _logger.warning("First import not completed"); return true; } + // Check if memories are enabled final areMemoriesShown = memoriesCacheService.showAnyMemories; if (!areMemoriesShown) { - _logger.warning("memories not enabled"); + _logger.warning("Memories not enabled"); return true; } return false; } - Future initMemoryHW(bool? forceFetchNewMemories) async { - final result = await hasAnyBlockers(); - if (result) { - await clearWidget(); + Future _forceMemoryUpdate() async { + await _loadAndRenderMemories(); + await updateMemoryChanged(false); + } + + Future _syncExistingMemories() async { + final homeWidgetCount = await countHomeWidgets(); + if (homeWidgetCount == 0) { + _logger.warning("No active home widgets found"); return; } - await _memoryForceRefreshLock.synchronized(() async { - final result = await hasAnyBlockers(); - if (result) { - return; - } - final isTotalEmpty = await _checkIfTotalEmpty(); - forceFetchNewMemories ??= await getForceFetchCondition(isTotalEmpty); - - _logger.warning( - "init memory hw: forceFetch: $forceFetchNewMemories, isTotalEmpty: $isTotalEmpty", - ); - - if (forceFetchNewMemories!) { - await _forceMemoryUpdate(); - } else if (!isTotalEmpty) { - await _memorySync(); - } - }); + await _refreshWidget(message: "Refreshing from existing memory set"); } - Future clearWidget() async { - final isTotalEmpty = await _checkIfTotalEmpty(); - if (isTotalEmpty) { - _logger.info(">>> Nothing to clear"); - return; - } - - _logger.info("Clearing MemoryHomeWidget"); - - await _setTotal(null); - _hasSyncedMemory = false; - - await _updateWidget(text: "MemoryHomeWidget cleared & updated"); - } - - Future updateMemoryChanged(bool value) async { - _logger.info("Updating memory changed to $value"); - await _prefs.setBool(memoryChangedKey, value); - } - - Future _checkIfTotalEmpty() async { - final total = await _getTotal(); + Future _isWidgetEmpty() async { + final total = await _getTotalMemories(); return total == 0 || total == null; } - Future getForceFetchCondition(bool isTotalEmpty) async { - final memoryChanged = _prefs.getBool(memoryChangedKey); - if (memoryChanged == true) return true; - - final cachedMemories = await memoriesCacheService.getCachedMemories(); - - final forceFetchNewMemories = - isTotalEmpty && (cachedMemories?.isNotEmpty ?? false); - return forceFetchNewMemories; - } - - Future checkPendingMemorySync() async { - await Future.delayed(const Duration(seconds: 5), () {}); - - final isTotalEmpty = await _checkIfTotalEmpty(); - final forceFetchNewMemories = await getForceFetchCondition(isTotalEmpty); - - if (_hasSyncedMemory && !forceFetchNewMemories) { - _logger.info(">>> Memory already synced"); - return; + Future _shouldForceFetchMemories(bool isWidgetEmpty) async { + // Check if memory changed flag is set + final memoryChanged = _prefs.getBool(MEMORY_CHANGED_KEY) ?? true; + if (memoryChanged == true) { + return true; + } + + final memoriesStatus = getMemoriesStatus(); + switch (memoriesStatus) { + case WidgetStatus.notSynced: + return true; + case WidgetStatus.syncedPartially: + return await countHomeWidgets() > 0; + case WidgetStatus.syncedEmpty: + case WidgetStatus.syncedAll: + return false; } - await HomeWidgetService.instance.initHomeWidget(); } - Future>> _getMemories() async { - final memories = await memoriesCacheService.getMemories(); + Future> _getMemoriesForWidget() async { + final lastYearValue = await getSelectedLastYearMemories(); + final mlValue = await getSelectedMLMemories(); + final onThisDayValue = await getSelectedOnThisDayMemories(); + final isMLEnabled = flagService.hasGrantedMLConsent; + + final memories = await memoriesCacheService.getMemoriesForWidget( + onThisDay: onThisDayValue ?? !isMLEnabled, + pastYears: lastYearValue ?? !isMLEnabled, + smart: mlValue ?? isMLEnabled, + ); + + return memories; + } + + Future>> _getMemoriesWithFiles() async { + final memories = await _getMemoriesForWidget(); + if (memories.isEmpty) { return {}; } - final files = Map.fromEntries( - memories.map((m) { - return MapEntry(m.title, m.memories.map((e) => e.file).toList()); - }), + return Map.fromEntries( + memories.map( + (memory) => + MapEntry(memory.title, memory.memories.map((m) => m.file).toList()), + ), ); - - return files; } - Future _updateWidget({String? text}) async { + Future _getTotalMemories() async { + return HomeWidgetService.instance.getData(TOTAL_MEMORIES_KEY); + } + + Future _setTotalMemories(int? total) async { + await HomeWidgetService.instance.setData(TOTAL_MEMORIES_KEY, total); + } + + Future _refreshWidget({String? message}) async { await HomeWidgetService.instance.updateWidget( - androidClass: "EnteMemoryWidgetProvider", - iOSClass: "EnteMemoryWidget", + androidClass: ANDROID_CLASS_NAME, + iOSClass: IOS_CLASS_NAME, ); + if (flagService.internalUser) { await Fluttertoast.showToast( - msg: "[i] ${text ?? "MemoryHomeWidget updated"}", + msg: "[i][mem] ${message ?? "MemoryHomeWidget updated"}", toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM, timeInSecForIosWeb: 1, @@ -167,98 +287,97 @@ class MemoryHomeWidgetService { fontSize: 16.0, ); } - _logger.info(">>> Home Widget updated, type: ${text ?? "normal"}"); + + _logger.info("Home Widget updated: ${message ?? "standard update"}"); } - Future memoryChanged() async { - final cachedMemories = await memoriesCacheService.getCachedMemories(); - final currentTotal = cachedMemories?.length ?? 0; + Future _loadAndRenderMemories() async { + final memoriesWithFiles = await _getMemoriesWithFiles(); - final int total = await _getTotal() ?? 0; - - if (total == currentTotal && total == 0) { - _logger.info(">>> Memories not changed, doing nothing"); - return; - } - - _logger.info(">>> Memories changed, updating widget"); - await updateMemoryChanged(true); - await initMemoryHW(true); - } - - Future _getTotal() async { - return HomeWidgetService.instance.getData(totalMemories); - } - - Future _setTotal(int? total) async => - await HomeWidgetService.instance.setData(totalMemories, total); - - Future _lockAndLoadMemories() async { - final files = await _getMemories(); - - if (files.isEmpty) { - _logger.warning("No files found, clearing everything"); + if (memoriesWithFiles.isEmpty) { + _logger.warning("No memories found, clearing widget"); await clearWidget(); return; } - final total = await _getTotal(); - _logger.info(">>> Total memories before: $total"); + final currentTotal = await _getTotalMemories(); + _logger.info("Current total memories in widget: $currentTotal"); - int index = 0; + final bool isWidgetPresent = await countHomeWidgets() > 0; - for (final i in files.entries) { - for (final file in i.value) { - final value = await HomeWidgetService.instance - .renderFile(file, "memory_widget_$index", i.key) - .catchError( - (e, sT) { - _logger.severe("Error rendering widget", e, sT); - return null; - }, - ); + final limit = isWidgetPresent ? MAX_MEMORIES_LIMIT : 5; + final maxAttempts = limit * 10; - if (value != null) { - final result = await hasAnyBlockers(); - if (result) { - return; - } - await _setTotal(index); - if (index == 1) { - await _updateWidget( - text: "First memory fetched. updating widget", - ); - } - index++; + int renderedCount = 0; + int attemptsCount = 0; - if (index >= 50) { - _logger.warning(">>> Max memory limit reached"); - break; - } + await updateMemoriesStatus(WidgetStatus.notSynced); + + final memoriesWithFilesLength = memoriesWithFiles.length; + final memoriesWithFilesEntries = memoriesWithFiles.entries.toList(); + final random = Random(); + + while (renderedCount < limit && attemptsCount < maxAttempts) { + final randomEntry = + memoriesWithFilesEntries[random.nextInt(memoriesWithFilesLength)]; + + if (randomEntry.value.isEmpty) continue; + + final randomMemoryFile = randomEntry.value.elementAt( + random.nextInt(randomEntry.value.length), + ); + final memoryTitle = randomEntry.key; + + final renderResult = await HomeWidgetService.instance + .renderFile( + randomMemoryFile, + "memory_widget_$renderedCount", + memoryTitle, + null, + ) + .catchError((e, stackTrace) { + _logger.severe("Error rendering widget", e, stackTrace); + return null; + }); + + if (renderResult != null) { + // Check for blockers again before continuing + if (await _hasAnyBlockers()) { + return; } + + await _setTotalMemories(renderedCount); + + // Show update toast after first item is rendered + if (renderedCount == 1) { + await _refreshWidget( + message: "First memory fetched, updating widget", + ); + await updateMemoriesStatus(WidgetStatus.syncedPartially); + } + + renderedCount++; } - if (index >= 50) { - break; - } + attemptsCount++; } - if (index == 0) { + if (attemptsCount >= maxAttempts) { + _logger.warning( + "Hit max attempts $maxAttempts. Only rendered $renderedCount of limit $limit.", + ); + } + + if (renderedCount == 0) { return; } - await _updateWidget( - text: ">>> Switching to next memory set, total: $index", - ); - } + if (isWidgetPresent) { + await updateMemoriesStatus(WidgetStatus.syncedAll); + } - Future onLaunchFromWidget(int generatedId, BuildContext context) async { - _hasSyncedMemory = true; - await _memorySync(); - - await memoriesCacheService.goToMemoryFromGeneratedFileID( - context, - generatedId, + await _refreshWidget( + message: "Switched to next memory set, total: $renderedCount", ); } } diff --git a/mobile/lib/services/notification_service.dart b/mobile/lib/services/notification_service.dart index 86cabfe014..74efedd00d 100644 --- a/mobile/lib/services/notification_service.dart +++ b/mobile/lib/services/notification_service.dart @@ -1,9 +1,12 @@ import 'dart:io'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import "package:flutter_timezone/flutter_timezone.dart"; import "package:logging/logging.dart"; import "package:photos/services/sync/remote_sync_service.dart"; import "package:shared_preferences/shared_preferences.dart"; +import 'package:timezone/data/latest_10y.dart' as tzdb; +import "package:timezone/timezone.dart" as tz; class NotificationService { static final NotificationService instance = @@ -24,11 +27,14 @@ class NotificationService { _preferences = preferences; } + bool timezoneInitialized = false; + Future initialize( void Function( NotificationResponse notificationResponse, ) onNotificationTapped, ) async { + await initTimezones(); const androidSettings = AndroidInitializationSettings('notification_icon'); const iosSettings = DarwinInitializationSettings( requestAlertPermission: false, @@ -59,6 +65,14 @@ class NotificationService { } } + Future initTimezones() async { + if (timezoneInitialized) return; + tzdb.initializeTimeZones(); + final String currentTimeZone = await FlutterTimezone.getLocalTimezone(); + tz.setLocalLocation(tz.getLocation(currentTimeZone)); + timezoneInitialized = true; + } + Future requestPermissions() async { bool? result; if (Platform.isIOS) { @@ -127,4 +141,142 @@ class NotificationService { payload: payload, ); } + + Future scheduleNotification( + String title, + String message, { + required int id, + String channelID = "io.ente.photos", + String channelName = "ente", + String payload = "ente://home", + required DateTime dateTime, + Duration? timeoutDurationAndroid, + }) async { + try { + _logger.info( + "Scheduling notification with: $title, $message, $channelID, $channelName, $payload", + ); + await initTimezones(); + if (!hasGrantedPermissions()) { + _logger.warning("Notification permissions not granted"); + await requestPermissions(); + if (!hasGrantedPermissions()) { + _logger.severe("Failed to get notification permissions"); + return; + } + } else { + _logger.info("Notification permissions already granted"); + } + final androidSpecs = AndroidNotificationDetails( + channelID, + channelName, + channelDescription: 'ente alerts', + importance: Importance.max, + priority: Priority.high, + category: AndroidNotificationCategory.reminder, + showWhen: false, + ); + final iosSpecs = DarwinNotificationDetails(threadIdentifier: channelID); + final platformChannelSpecs = + NotificationDetails(android: androidSpecs, iOS: iosSpecs); + final scheduledDate = tz.TZDateTime.local( + dateTime.year, + dateTime.month, + dateTime.day, + dateTime.hour, + dateTime.minute, + dateTime.second, + ); + // final tz.TZDateTime scheduledDate = tz.TZDateTime.now(tz.local).add(delay); + await _notificationsPlugin.zonedSchedule( + id, + title, + message, + scheduledDate, + platformChannelSpecs, + androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle, + payload: payload, + ); + _logger.info( + "Scheduled notification with: $title, $message, $channelID, $channelName, $payload for $dateTime", + ); + } catch (e, s) { + // For now we're swallowing any exceptions here because we don't want the memories logic to get disturbed + _logger.severe( + "Something went wrong while scheduling notification", + e, + s, + ); + } + final androidSpecs = AndroidNotificationDetails( + channelID, + channelName, + channelDescription: 'ente alerts', + importance: Importance.max, + priority: Priority.high, + category: AndroidNotificationCategory.reminder, + showWhen: false, + timeoutAfter: timeoutDurationAndroid?.inMilliseconds, + ); + final iosSpecs = DarwinNotificationDetails(threadIdentifier: channelID); + final platformChannelSpecs = + NotificationDetails(android: androidSpecs, iOS: iosSpecs); + final scheduledDate = tz.TZDateTime.local( + dateTime.year, + dateTime.month, + dateTime.day, + dateTime.hour, + dateTime.minute, + dateTime.second, + ); + // final tz.TZDateTime scheduledDate = tz.TZDateTime.now(tz.local).add(delay); + await _notificationsPlugin.zonedSchedule( + id, + title, + message, + scheduledDate, + platformChannelSpecs, + androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle, + payload: payload, + ); + _logger.info( + "Scheduled notification with: $title, $message, $channelID, $channelName, $payload", + ); + } + + Future clearAllScheduledNotifications({ + String? containingPayload, + }) async { + try { + _logger.info("Clearing all scheduled notifications"); + final pending = await _notificationsPlugin.pendingNotificationRequests(); + if (pending.isEmpty) { + _logger.info("No pending notifications to clear"); + return; + } + for (final request in pending) { + if (containingPayload != null && + !request.payload.toString().contains(containingPayload)) { + _logger.info( + "Skip clearing of notification with id: ${request.id} and payload: ${request.payload}", + ); + continue; + } + _logger.info( + "Clearing notification with id: ${request.id} and payload: ${request.payload}", + ); + await _notificationsPlugin.cancel(request.id); + _logger.info( + "Cleared notification with id: ${request.id} and payload: ${request.payload}", + ); + } + } catch (e, s) { + _logger.severe("Something is wrong with scheduled notifications", e, s); + } + } + + Future pendingNotifications() async { + final pending = await _notificationsPlugin.pendingNotificationRequests(); + return pending.length; + } } diff --git a/mobile/lib/services/people_home_widget_service.dart b/mobile/lib/services/people_home_widget_service.dart new file mode 100644 index 0000000000..ee758caac0 --- /dev/null +++ b/mobile/lib/services/people_home_widget_service.dart @@ -0,0 +1,457 @@ +import "dart:math"; + +import "package:collection/collection.dart"; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:logging/logging.dart'; +import 'package:photos/db/files_db.dart'; +import 'package:photos/models/file/file.dart'; +import "package:photos/models/search/generic_search_result.dart"; +import "package:photos/models/search/hierarchical/face_filter.dart"; +import "package:photos/models/search/search_types.dart"; +import 'package:photos/service_locator.dart'; +import 'package:photos/services/home_widget_service.dart'; +import 'package:photos/services/machine_learning/face_ml/person/person_service.dart'; +import 'package:photos/services/search_service.dart'; +import 'package:photos/services/sync/local_sync_service.dart'; +import "package:photos/ui/viewer/file/detail_page.dart"; +import "package:photos/ui/viewer/people/people_page.dart"; +import 'package:photos/utils/navigation_util.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:synchronized/synchronized.dart'; + +class PeopleHomeWidgetService { + // Constants + static const String SELECTED_PEOPLE_KEY = "selectedPeopleHW"; + static const String ANDROID_CLASS_NAME = "EntePeopleWidgetProvider"; + static const String IOS_CLASS_NAME = "EntePeopleWidget"; + static const String PEOPLE_STATUS_KEY = "peopleStatusKey.widget"; + static const String PEOPLE_CHANGED_KEY = "peopleChanged.widget"; + static const String TOTAL_PEOPLE_KEY = "totalPeople"; + static const int MAX_PEOPLE_LIMIT = 50; + + // Singleton pattern + static final PeopleHomeWidgetService instance = + PeopleHomeWidgetService._privateConstructor(); + PeopleHomeWidgetService._privateConstructor(); + + // Properties + final Logger _logger = Logger((PeopleHomeWidgetService).toString()); + late final SharedPreferences _prefs; + final _peopleForceRefreshLock = Lock(); + bool _hasSyncedPeople = false; + + // Initialization + void init(SharedPreferences prefs) { + _prefs = prefs; + } + + // Public methods + List? getSelectedPeople() { + return _prefs.getStringList(SELECTED_PEOPLE_KEY); + } + + Future setSelectedPeople(List selectedPeople) async { + final previousSelection = getSelectedPeople(); + await _prefs.setStringList(SELECTED_PEOPLE_KEY, selectedPeople); + + if (previousSelection != null) { + final oldSet = previousSelection.toSet(); + final newSet = selectedPeople.toSet(); + + if (oldSet.containsAll(newSet) && newSet.containsAll(oldSet)) { + _logger.info("People selection unchanged, no update needed"); + return; + } + } + + _logger.info("People selection changed, updating widget"); + await updatePeopleChanged(true); + } + + Future countHomeWidgets() async { + return await HomeWidgetService.instance.countHomeWidgets( + ANDROID_CLASS_NAME, + IOS_CLASS_NAME, + ); + } + + Future initHomeWidget(bool? forceFetchNewPeople) async { + if (await _hasAnyBlockers()) { + await clearWidget(); + return; + } + + await _peopleForceRefreshLock.synchronized(() async { + if (await _hasAnyBlockers()) { + return; + } + + final isPeopleEmpty = await _isWidgetEmpty(); + forceFetchNewPeople ??= await _shouldForceFetchPeople(isPeopleEmpty); + + _logger.warning( + "Initializing people widget: forceFetch: $forceFetchNewPeople, isPeopleEmpty: $isPeopleEmpty", + ); + + if (forceFetchNewPeople!) { + await _forcePeopleUpdate(); + } else if (!isPeopleEmpty) { + await _syncExistingPeople(); + } + }); + } + + Future clearWidget() async { + if (await _isWidgetEmpty()) { + _logger.info("Widget already empty, nothing to clear"); + return; + } + + _logger.info("Clearing PeopleHomeWidget"); + await _setTotalPeople(null); + _hasSyncedPeople = false; + await updatePeopleStatus(WidgetStatus.syncedEmpty); + await _refreshWidget(message: "PeopleHomeWidget cleared & updated"); + } + + WidgetStatus getPeopleStatus() { + return WidgetStatus.values.firstWhereOrNull( + (v) => v.index == (_prefs.getInt(PEOPLE_STATUS_KEY) ?? 0), + ) ?? + WidgetStatus.notSynced; + } + + Future updatePeopleStatus(WidgetStatus value) async { + await _prefs.setInt(PEOPLE_STATUS_KEY, value.index); + } + + Future updatePeopleChanged(bool value) async { + _logger.info("Updating people changed flag to $value"); + await _prefs.setBool(PEOPLE_CHANGED_KEY, value); + } + + Future checkPendingPeopleSync({bool addDelay = true}) async { + if (addDelay) { + await Future.delayed(const Duration(seconds: 5)); + } + + final isPeopleEmpty = await _isWidgetEmpty(); + final needsForceFetch = await _shouldForceFetchPeople(isPeopleEmpty); + + if (_hasSyncedPeople && !needsForceFetch) { + _logger.info("People already synced, no action needed"); + return; + } + + await HomeWidgetService.instance.initHomeWidget(); + } + + Future peopleChanged() async { + await updatePeopleChanged(true); + + final cachedMemories = await _getPeople(); + final currentTotal = cachedMemories.length; + final existingTotal = await _getTotalPeople() ?? 0; + + if (existingTotal == currentTotal && existingTotal == 0) { + await updatePeopleChanged(false); + _logger.info("People empty, no update needed"); + return; + } + + _logger.info("People changed, updating widget"); + await initHomeWidget(true); + } + + Future onLaunchFromWidget( + int fileId, + String personId, + BuildContext context, + ) async { + _hasSyncedPeople = true; + await _syncExistingPeople(); + + final file = await FilesDB.instance.getFile(fileId); + if (file == null) { + _logger.warning("Cannot launch widget: file with ID $fileId not found"); + return; + } + + final person = await PersonService.instance.getPerson(personId); + if (person == null) { + _logger + .warning("Cannot launch widget: person with ID $personId not found"); + return; + } + + routeToPage( + context, + PeoplePage( + person: person, + searchResult: null, + ), + forceCustomPageRoute: true, + ).ignore(); + + final clusterFiles = + await SearchService.instance.getClusterFilesForPersonID( + personId, + ); + final files = clusterFiles.entries.expand((e) => e.value).toList(); + + await routeToPage( + context, + DetailPage( + DetailPageConfiguration( + files, + files.indexOf(file), + "peoplewidget", + ), + ), + forceCustomPageRoute: true, + ); + } + + // Private methods + Future _forcePeopleUpdate() async { + await _loadAndRenderPeople(); + await updatePeopleChanged(false); + } + + Future _hasAnyBlockers() async { + // Check if first import is completed + final hasCompletedFirstImport = + LocalSyncService.instance.hasCompletedFirstImport(); + if (!hasCompletedFirstImport) { + _logger.warning("First import not completed"); + return true; + } + + // Check ML consent + if (!flagService.hasGrantedMLConsent) { + _logger.warning("ML consent not granted"); + return true; + } + + // Check if selected people exist + final peopleIds = await _getEffectiveSelectedPeopleIds(); + try { + for (final id in peopleIds) { + final person = await PersonService.instance.getPerson(id); + if (person == null) { + _logger.warning("Person not found for id: $id"); + return true; + } + } + } catch (e) { + _logger.warning("Error looking up people: $e"); + return true; + } + + return false; + } + + Future _syncExistingPeople() async { + final homeWidgetCount = await countHomeWidgets(); + if (homeWidgetCount == 0) { + _logger.warning("No active home widgets found"); + return; + } + + await _refreshWidget(message: "Refreshing from existing people set"); + } + + Future _isWidgetEmpty() async { + final totalPeople = await _getTotalPeople(); + return totalPeople == 0 || totalPeople == null; + } + + Future _shouldForceFetchPeople(bool isPeopleEmpty) async { + final peopleChanged = _prefs.getBool(PEOPLE_CHANGED_KEY); + if (peopleChanged ?? true) { + return true; + } + + final peopleStatus = getPeopleStatus(); + switch (peopleStatus) { + case WidgetStatus.notSynced: + return true; + case WidgetStatus.syncedPartially: + return await countHomeWidgets() > 0; + case WidgetStatus.syncedEmpty: + case WidgetStatus.syncedAll: + return false; + } + } + + Future> _getEffectiveSelectedPeopleIds() async { + var peopleIds = getSelectedPeople(); + + if (peopleIds == null || peopleIds.isEmpty) { + // Search Filter with face and pick top two faces + final searchFilter = await SectionType.face.getData(null).then( + (value) => List.from(value).where( + (element) => + (element.hierarchicalSearchFilter as FaceFilter).personId != + null, + ), + ); + + if (searchFilter.isNotEmpty) { + peopleIds = searchFilter + .take(2) + .map((e) => (e.hierarchicalSearchFilter as FaceFilter).personId!) + .toList(); + } else { + _logger.warning("No selected people found"); + } + } + + return peopleIds ?? []; + } + + Future)>> _getPeople() async { + final peopleIds = await _getEffectiveSelectedPeopleIds(); + final Map)> peopleFiles = {}; + + for (final id in peopleIds) { + final person = await PersonService.instance.getPerson(id); + if (person == null) { + _logger.warning("Person not found for id: $id"); + continue; + } + + final clusterFiles = + await SearchService.instance.getClusterFilesForPersonID(id); + final files = clusterFiles.entries.expand((e) => e.value).toList(); + if (files.isEmpty) { + _logger.warning("No files found for person: ${person.data.name}"); + continue; + } + peopleFiles[id] = (person.data.name, files); + } + + return peopleFiles; + } + + Future _refreshWidget({String? message}) async { + await HomeWidgetService.instance.updateWidget( + androidClass: ANDROID_CLASS_NAME, + iOSClass: IOS_CLASS_NAME, + ); + + if (flagService.internalUser) { + await Fluttertoast.showToast( + msg: "[i][ppl] ${message ?? "PeopleHomeWidget updated"}", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: Colors.black, + textColor: Colors.white, + fontSize: 16.0, + ); + } + + _logger.info("Home Widget updated: ${message ?? "standard update"}"); + } + + Future _getTotalPeople() async { + return HomeWidgetService.instance.getData(TOTAL_PEOPLE_KEY); + } + + Future _setTotalPeople(int? total) async { + await HomeWidgetService.instance.setData(TOTAL_PEOPLE_KEY, total); + } + + Future _loadAndRenderPeople() async { + final peopleWithFiles = await _getPeople(); + + if (peopleWithFiles.isEmpty) { + _logger.warning("No files found for any people, clearing widget"); + await clearWidget(); + return; + } + + final currentTotal = await _getTotalPeople(); + _logger.info("Current total people in widget: $currentTotal"); + + final bool isWidgetPresent = await countHomeWidgets() > 0; + + final limit = isWidgetPresent ? MAX_PEOPLE_LIMIT : 5; + final maxAttempts = limit * 10; + + int renderedCount = 0; + int attemptsCount = 0; + + await updatePeopleStatus(WidgetStatus.notSynced); + + final peopleWithFilesLength = peopleWithFiles.length; + final peopleWithFilesEntries = peopleWithFiles.entries.toList(); + final random = Random(); + + while (renderedCount < limit && attemptsCount < maxAttempts) { + final randomEntry = + peopleWithFilesEntries[random.nextInt(peopleWithFilesLength)]; + + if (randomEntry.value.$2.isEmpty) continue; + + final randomPersonFile = randomEntry.value.$2.elementAt( + random.nextInt(randomEntry.value.$2.length), + ); + final personId = randomEntry.key; + final personName = randomEntry.value.$1; + + final renderResult = await HomeWidgetService.instance + .renderFile( + randomPersonFile, + "people_widget_$renderedCount", + personName, + personId, + ) + .catchError((e, stackTrace) { + _logger.severe("Error rendering widget", e, stackTrace); + return null; + }); + + if (renderResult != null) { + // Check for blockers again before continuing + if (await _hasAnyBlockers()) { + return; + } + + await _setTotalPeople(renderedCount); + + // Show update toast after first item is rendered + if (renderedCount == 1) { + await _refreshWidget( + message: "First person fetched, updating widget", + ); + await updatePeopleStatus(WidgetStatus.syncedPartially); + } + + renderedCount++; + } + + attemptsCount++; + } + + if (attemptsCount >= maxAttempts) { + _logger.warning( + "Hit max attempts $maxAttempts. Only rendered $renderedCount of limit $limit.", + ); + } + + if (renderedCount == 0) { + return; + } + + if (isWidgetPresent) { + await updatePeopleStatus(WidgetStatus.syncedAll); + } + + await _refreshWidget( + message: "Switched to next people set, total: $renderedCount", + ); + } +} diff --git a/mobile/lib/services/preview_video_store.dart b/mobile/lib/services/preview_video_store.dart index 1c057f2cb3..4a9a6df904 100644 --- a/mobile/lib/services/preview_video_store.dart +++ b/mobile/lib/services/preview_video_store.dart @@ -21,7 +21,6 @@ import "package:photos/core/event_bus.dart"; import "package:photos/core/network/network.dart"; import 'package:photos/db/files_db.dart'; import "package:photos/db/upload_locks_db.dart"; -import "package:photos/events/preview_updated_event.dart"; import "package:photos/events/video_streaming_changed.dart"; import "package:photos/models/base/id.dart"; import "package:photos/models/ffmpeg/ffprobe_props.dart"; @@ -30,7 +29,9 @@ import "package:photos/models/file/file_type.dart"; import "package:photos/models/preview/playlist_data.dart"; import "package:photos/models/preview/preview_item.dart"; import "package:photos/models/preview/preview_item_status.dart"; -import "package:photos/services/filedata/filedata_service.dart"; +import "package:photos/service_locator.dart"; +import "package:photos/services/filedata/model/file_data.dart"; +import "package:photos/services/machine_learning/ml_service.dart"; import "package:photos/ui/notification/toast.dart"; import "package:photos/utils/exif_util.dart"; import "package:photos/utils/file_key.dart"; @@ -43,10 +44,11 @@ const _maxRetryCount = 3; class PreviewVideoStore { final LinkedHashMap _items = LinkedHashMap(); - LinkedHashMap get previews => _items; + LinkedHashMap fileQueue = LinkedHashMap(); + final int _minPreviewSizeForCache = 50 * 1024 * 1024; // 50 MB Set? _failureFiles; - bool _initSuccess = false; + bool _hasQueuedFile = false; PreviewVideoStore._privateConstructor(); @@ -57,7 +59,6 @@ class PreviewVideoStore { final cacheManager = DefaultCacheManager(); final videoCacheManager = VideoCacheManager.instance; - LinkedHashSet fileQueue = LinkedHashSet(); int uploadingFileId = -1; final _dio = NetworkClient.instance.enteDio; @@ -86,7 +87,7 @@ class PreviewVideoStore { Bus.instance.fire(VideoStreamingChanged()); if (isVideoStreamingEnabled) { - await FileDataService.instance.syncFDStatus(); + await fileDataService.syncFDStatus(); _putFilesForPreviewCreation().ignore(); } else { clearQueue(); @@ -96,7 +97,7 @@ class PreviewVideoStore { void clearQueue() { fileQueue.clear(); _items.clear(); - Bus.instance.fire(PreviewUpdatedEvent(_items)); + _hasQueuedFile = false; } DateTime? get videoStreamingCutoff { @@ -107,7 +108,10 @@ class PreviewVideoStore { Future isSharedFileStreamble(EnteFile file) async { try { - await getPreviewUrl(file); + if (fileDataService.previewIds.containsKey(file.uploadedFileID)) { + return true; + } + await _getPreviewUrl(file); return true; } catch (_) { return false; @@ -119,7 +123,10 @@ class PreviewVideoStore { EnteFile enteFile, [ bool forceUpload = false, ]) async { - if (!isVideoStreamingEnabled) { + if (!isVideoStreamingEnabled || MLService.instance.isRunningML) { + _logger.info( + "Pause preview due to disabledSteaming($isVideoStreamingEnabled) or MLRunning (${MLService.instance.isRunningML})", + ); clearQueue(); return; } @@ -131,17 +138,15 @@ class PreviewVideoStore { removeFile = true; return; } - try { // check if playlist already exist - await getPlaylist(enteFile); - final _ = await getPreviewUrl(enteFile); - - if (ctx != null && ctx.mounted) { - showShortToast(ctx, 'Video preview already exists'); + if (await getPlaylist(enteFile) != null) { + if (ctx != null && ctx.mounted) { + showShortToast(ctx, 'Video preview already exists'); + } + removeFile = true; + return; } - removeFile = true; - return; } catch (e, s) { if (e is DioException && e.response?.statusCode == 404) { _logger.info("No preview found for $enteFile"); @@ -151,7 +156,6 @@ class PreviewVideoStore { return; } } - // elimination case for <=10 MB with H.264 var (props, result, file) = await _checkFileForPreviewCreation(enteFile); if (result) { @@ -171,8 +175,7 @@ class PreviewVideoStore { : _items[enteFile.uploadedFileID!]?.retryCount ?? 0, collectionID: enteFile.collectionID ?? 0, ); - Bus.instance.fire(PreviewUpdatedEvent(_items)); - fileQueue.add(enteFile); + fileQueue[enteFile.uploadedFileID!] = enteFile; return; } @@ -185,7 +188,6 @@ class PreviewVideoStore { forceUpload ? 0 : _items[enteFile.uploadedFileID!]?.retryCount ?? 0, collectionID: enteFile.collectionID ?? 0, ); - Bus.instance.fire(PreviewUpdatedEvent(_items)); // get file file ??= await getFile(enteFile, isOrigin: true); @@ -293,7 +295,6 @@ class PreviewVideoStore { collectionID: enteFile.collectionID ?? 0, retryCount: _items[enteFile.uploadedFileID!]?.retryCount ?? 0, ); - Bus.instance.fire(PreviewUpdatedEvent(_items)); _logger.info('Playlist Generated ${enteFile.displayName}'); @@ -351,7 +352,7 @@ class PreviewVideoStore { if (error == null) { // update previewIds - FileDataService.instance.appendPreview( + fileDataService.appendPreview( enteFile.uploadedFileID!, objectId!, objectSize!, @@ -365,17 +366,13 @@ class PreviewVideoStore { ); _removeFromLocks(enteFile).ignore(); Directory(prefix).delete(recursive: true).ignore(); - - Bus.instance.fire(PreviewUpdatedEvent(_items)); } } finally { if (error != null) { _retryFile(enteFile, error); - Bus.instance.fire(PreviewUpdatedEvent(_items)); } else if (removeFile) { _removeFile(enteFile); _removeFromLocks(enteFile).ignore(); - Bus.instance.fire(PreviewUpdatedEvent(_items)); } // reset uploading status if this was getting processed if (uploadingFileId == enteFile.uploadedFileID!) { @@ -384,8 +381,9 @@ class PreviewVideoStore { _logger.info("[chunk] Processing ${_items.length} items for streaming"); // process next file if (fileQueue.isNotEmpty) { - final file = fileQueue.first; - fileQueue.remove(file); + final entry = fileQueue.entries.first; + final file = entry.value; + fileQueue.remove(entry.key); await chunkAndUploadVideo(ctx, file); } } @@ -414,7 +412,7 @@ class PreviewVideoStore { retryCount: _items[enteFile.uploadedFileID!]!.retryCount + 1, collectionID: enteFile.collectionID ?? 0, ); - fileQueue.add(enteFile); + fileQueue[enteFile.uploadedFileID!] = enteFile; } else { _items[enteFile.uploadedFileID!] = PreviewItem( status: PreviewItemStatus.failed, @@ -529,16 +527,25 @@ class PreviewVideoStore { _logger.info("Getting playlist for $file"); int? width, height, size; try { - final objectKey = - FileDataService.instance.previewIds?[file.uploadedFileID!]?.objectId; - final FileInfo? playlistCache = (objectKey == null) - ? null - : await cacheManager.getFileFromCache(_getCacheKey(objectKey)); - final detailsCache = (objectKey == null) - ? null - : await cacheManager.getFileFromCache( - _getDetailsCacheKey(objectKey), - ); + late final String objectID; + final PreviewInfo? previewInfo = + fileDataService.previewIds[file.uploadedFileID!]; + bool shouldAppendPreview = false; + (String, String)? previewURLResult; + if (previewInfo == null) { + shouldAppendPreview = true; + previewURLResult = await _getPreviewUrl(file); + _logger.info("parrsed objectID: ${previewURLResult.$2}"); + objectID = previewURLResult.$2; + } else { + objectID = previewInfo.objectId; + } + + final FileInfo? playlistCache = + await cacheManager.getFileFromCache(_getCacheKey(objectID)); + final detailsCache = await cacheManager.getFileFromCache( + _getDetailsCacheKey(objectID), + ); String finalPlaylist; if (playlistCache != null) { finalPlaylist = playlistCache.file.readAsStringSync(); @@ -565,87 +572,99 @@ class PreviewVideoStore { header: header, ); finalPlaylist = playlistData["playlist"]; - width = playlistData["width"]; height = playlistData["height"]; size = playlistData["size"]; - - if (objectKey != null) { - unawaited( - cacheManager.putFile( - _getCacheKey(objectKey), - Uint8List.fromList( - (playlistData["playlist"] as String).codeUnits, - ), + unawaited( + cacheManager.putFile( + _getCacheKey(objectID), + Uint8List.fromList( + (playlistData["playlist"] as String).codeUnits, ), - ); - final details = { - "width": width, - "height": height, - "size": size, - }; - unawaited( - cacheManager.putFile( - _getDetailsCacheKey(objectKey), - Uint8List.fromList( - json.encode(details).codeUnits, - ), - ), - ); - } - } - - final videoFile = objectKey == null - ? null - : (await videoCacheManager - .getFileFromCache(_getVideoPreviewKey(objectKey))) - ?.file; - if (videoFile == null) { - final response2 = await _dio.get( - "/files/data/preview", - queryParameters: { - "fileID": file.uploadedFileID, - "type": "vid_preview", - }, + ), ); - final previewURL = response2.data["url"]; - if (objectKey != null) { + unawaited( + cacheManager.putFile( + _getDetailsCacheKey(objectID), + Uint8List.fromList( + json.encode({ + "width": width, + "height": height, + "size": size, + }).codeUnits, + ), + ), + ); + } + final videoFile = (await videoCacheManager + .getFileFromCache(_getVideoPreviewKey(objectID))) + ?.file; + if (videoFile == null) { + previewURLResult = previewURLResult ?? await _getPreviewUrl(file); + if (size != null && size < _minPreviewSizeForCache) { unawaited( - _downloadAndCacheVideo( - previewURL, - _getVideoPreviewKey(objectKey), + videoCacheManager.downloadFile( + previewURLResult.$1, + key: _getVideoPreviewKey(objectID), ), ); } finalPlaylist = - finalPlaylist.replaceAll('\noutput.ts', '\n$previewURL'); + finalPlaylist.replaceAll('\noutput.ts', '\n${previewURLResult.$1}'); } else { finalPlaylist = finalPlaylist.replaceAll('\noutput.ts', '\n${videoFile.path}'); } - final tempDir = await getTemporaryDirectory(); final playlistFile = File("${tempDir.path}/${file.uploadedFileID}.m3u8"); await playlistFile.writeAsString(finalPlaylist); - _logger.info("Writing playlist to ${playlistFile.path}"); + final String log = (StringBuffer() + ..write("[CACHE-STATUS] ") + ..write("Video: ${videoFile != null ? '✓' : '✗'} | ") + ..write("Details: ${detailsCache != null ? '✓' : '✗'} | ") + ..write("Playlist: ${playlistCache != null ? '✓' : '✗'}")) + .toString(); + _logger.info("Mapped playlist to ${playlistFile.path}, $log"); final data = PlaylistData( preview: playlistFile, width: width, height: height, size: size, + durationInSeconds: parseDurationFromHLS(finalPlaylist), ); + if (shouldAppendPreview) { + fileDataService.appendPreview( + file.uploadedFileID!, + objectID, + size!, + ); + } return data; } catch (_) { rethrow; } } - Future _downloadAndCacheVideo(String url, String key) async { - final file = await videoCacheManager.downloadFile(url, key: key); - return file; + int? parseDurationFromHLS(String playlist) { + final lines = playlist.split("\n"); + double totalDuration = 0.0; + for (final line in lines) { + if (line.startsWith("#EXTINF:")) { + // Extract duration value (e.g., "#EXTINF:2.400000," → "2.400000") + final durationStr = line.substring( + 8, + line.length - 1, + ); + final duration = double.tryParse(durationStr); + if (duration != null) { + totalDuration += duration; + } + } + } + return totalDuration > 0 ? totalDuration.round() : null; } - Future getPreviewUrl(EnteFile file) async { + Future<(String, String)> _getPreviewUrl(EnteFile file) async { try { final response = await _dio.get( "/files/data/preview", @@ -655,7 +674,13 @@ class PreviewVideoStore { file.fileType == FileType.video ? "vid_preview" : "img_preview", }, ); - return response.data["url"]; + + final url = (response.data["url"] as String); + final uri = Uri.parse(url); + final segments = uri.pathSegments; + if (segments.isEmpty) throw Exception("Invalid URL"); + final String objectID = segments.last; + return (url, objectID); } catch (e) { _logger.warning("Failed to get preview url", e); rethrow; @@ -665,20 +690,37 @@ class PreviewVideoStore { Future<(FFProbeProps?, bool, File?)> _checkFileForPreviewCreation( EnteFile enteFile, ) async { - final fileSize = enteFile.fileSize; + if ((enteFile.pubMagicMetadata?.sv ?? 0) == 1) { + _logger.info( + "Skip Preview due to sv=1 for ${enteFile.displayName}", + ); + return (null, true, null); + } + if (enteFile.fileSize == null || enteFile.duration == null) { + _logger.warning( + "Skip Preview due to misisng size/duration for ${enteFile.displayName}", + ); + return (null, true, null); + } + final int size = enteFile.fileSize!; + final int duration = enteFile.duration!; + if (size >= 500 * 1024 * 1024 || duration > 60) { + _logger.info( + "Skip Preview due to size: $size or duration: $duration", + ); + return (null, true, null); + } FFProbeProps? props; File? file; bool skipFile = false; - try { - final isFileUnder10MB = fileSize != null && fileSize <= 10 * 1024 * 1024; + final isFileUnder10MB = size <= 10 * 1024 * 1024; if (isFileUnder10MB) { file = await getFile(enteFile, isOrigin: true); if (file != null) { props = await getVideoPropsAsync(file); final videoData = List.from(props?.propData?["streams"] ?? []) .firstWhereOrNull((e) => e["type"] == "video"); - final codec = videoData["codec_name"]?.toString().toLowerCase(); skipFile = codec?.contains("h264") ?? false; @@ -690,28 +732,6 @@ class PreviewVideoStore { } } } - - int? size = enteFile.fileSize; - int? duration = enteFile.duration; - - if (size == null) { - file = await getFile(enteFile, isOrigin: true); - size = file?.lengthSync(); - } - - if (duration == null) { - file ??= await getFile(enteFile, isOrigin: true); - props = await getVideoPropsAsync(file!); - duration = props?.duration?.inSeconds; - } - - if ((size == null || duration == null) || - (size >= 500 * 1024 * 1024 || duration > 60)) { - skipFile = true; - _logger.info( - "[init] Ignoring file ${enteFile.displayName} for preview due to size: $size and duration: $duration", - ); - } } catch (e, sT) { _logger.warning("Failed to check props", e, sT); } @@ -724,7 +744,7 @@ class PreviewVideoStore { final cutoff = videoStreamingCutoff; if (cutoff == null) return; - if (updateInit) _initSuccess = true; + if (updateInit) _hasQueuedFile = true; Map failureFiles = {}; try { @@ -733,7 +753,7 @@ class PreviewVideoStore { // handle case when failures are already previewed for (final failure in _failureFiles!) { - if (previews.containsKey(failure)) { + if (_items.containsKey(failure)) { UploadLocksDB.instance.deleteStreamUploadErrorEntry(failure).ignore(); } } @@ -745,28 +765,16 @@ class PreviewVideoStore { userID: Configuration.instance.getUserID()!, ); - final previewIds = FileDataService.instance.previewIds; - final allFiles = files - .where((file) => previewIds?[file.uploadedFileID] == null) - .sorted((a, b) { - // put higher duration videos last along with remote files - final first = (a.localID == null ? 2 : 0) + - (a.duration == null || a.duration! >= 10 * 60 ? 1 : 0); - final second = (b.localID == null ? 2 : 0) + - (b.duration == null || b.duration! >= 10 * 60 ? 1 : 0); - return first.compareTo(second); - }).toList(); + final previewIds = fileDataService.previewIds; + final allFiles = + files.where((file) => previewIds[file.uploadedFileID] == null).toList(); // set all video status to in queue var n = allFiles.length, i = 0; while (i < n) { final enteFile = allFiles[i]; - - // elimination case for <=10 MB with H.264 - final (_, result, _) = await _checkFileForPreviewCreation(enteFile); final isFailure = _failureFiles?.contains(enteFile.uploadedFileID!) ?? false; - if (isFailure) { _items[enteFile.uploadedFileID!] = PreviewItem( status: PreviewItemStatus.failed, @@ -776,7 +784,7 @@ class PreviewVideoStore { error: failureFiles[enteFile.uploadedFileID!], ); } - if (result || isFailure) { + if (isFailure) { _logger.info( "[init] Ignoring file ${enteFile.displayName} for preview", ); @@ -794,7 +802,6 @@ class PreviewVideoStore { i++; } - Bus.instance.fire(PreviewUpdatedEvent(_items)); if (allFiles.isEmpty) { _logger.info("[init] No preview to cache"); return; @@ -804,15 +811,22 @@ class PreviewVideoStore { // take first file and put it for stream generation final file = allFiles.removeAt(0); - fileQueue.addAll(allFiles); + for (final enteFile in allFiles) { + if (_items.containsKey(enteFile.uploadedFileID!)) { + continue; + } + fileQueue[enteFile.uploadedFileID!] = enteFile; + } chunkAndUploadVideo(null, file).ignore(); } void queueFiles() { - if (!_initSuccess) { - _putFilesForPreviewCreation(true).catchError((_) { - _initSuccess = false; - }); - } + Future.delayed(const Duration(seconds: 5), () { + if (!_hasQueuedFile) { + _putFilesForPreviewCreation(true).catchError((_) { + _hasQueuedFile = false; + }); + } + }); } } diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 1c2e6ed2ab..a2191526f0 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -5,6 +5,7 @@ import "package:flutter/cupertino.dart"; import "package:flutter/material.dart"; import "package:intl/intl.dart"; import 'package:logging/logging.dart'; +import "package:photos/core/configuration.dart"; import "package:photos/core/constants.dart"; import 'package:photos/core/event_bus.dart'; import 'package:photos/data/holidays.dart'; @@ -1272,7 +1273,7 @@ class SearchService { final cache = await memoriesCacheService.debugCacheForTesting(); final memoriesResult = await smartMemoriesService - .calcMemories(calcTime, cache, debugSurfaceAll: true); + .calcSmartMemories(calcTime, cache, debugSurfaceAll: true); locationService.baseLocations = memoriesResult.baseLocations; memories = memoriesResult.memories; } @@ -1413,10 +1414,29 @@ class SearchService { int? limit, ) async { try { + final int ownerID = Configuration.instance.getUserID()!; final searchResults = []; final allFiles = await getAllFilesForSearch(); final peopleToSharedFiles = >{}; + final peopleToSharedAlbums = >{}; final existingEmails = {}; + final familyEmails = UserService.instance.getEmailIDsOfFamilyMember(); + final List collections = _collectionService + .getCollectionsForUI(includedShared: true, includeCollab: true); + + for (Collection collection in collections) { + if (collection.isHidden() || + collection.isArchived() || + collection.isOwner(ownerID)) { + continue; + } + + if (peopleToSharedAlbums.containsKey(collection.owner.email)) { + peopleToSharedAlbums[collection.owner.email]!.add(collection); + } else { + peopleToSharedAlbums[collection.owner.email] = [collection]; + } + } int peopleCount = 0; for (EnteFile file in allFiles) { @@ -1460,31 +1480,55 @@ class SearchService { } } - peopleToSharedFiles.forEach((key, value) { - final name = key.displayName != null && key.displayName!.isNotEmpty - ? key.displayName! - : key.email; + final sortedEntries = peopleToSharedFiles.entries.toList(); + sortedEntries.sort((a, b) { + final isAFamily = familyEmails.contains(a.key.email); + final isBFamily = familyEmails.contains(b.key.email); + if (isAFamily != isBFamily) { + return isAFamily ? -1 : 1; + } + + final countComparison = b.value.length.compareTo(a.value.length); + if (countComparison != 0) { + return countComparison; + } + + final aName = + a.key.displayName?.toLowerCase() ?? a.key.email.toLowerCase(); + final bName = + b.key.displayName?.toLowerCase() ?? b.key.email.toLowerCase(); + return aName.compareTo(bName); + }); + + for (var entry in sortedEntries) { + final user = entry.key; + final files = entry.value; + final name = user.displayName != null && user.displayName!.isNotEmpty + ? user.displayName! + : user.email; + final collections = peopleToSharedAlbums[user.email] ?? []; searchResults.add( GenericSearchResult( ResultType.shared, name, - value, + files, hierarchicalSearchFilter: ContactsFilter( - user: key, + user: user, occurrence: kMostRelevantFilter, - matchedUploadedIDs: filesToUploadedFileIDs(value), + matchedUploadedIDs: filesToUploadedFileIDs(files), ), params: { - kPersonParamID: key.linkedPersonID, - kContactEmail: key.email, + kPersonParamID: user.linkedPersonID, + kContactEmail: user.email, + kContactCollections: collections, }, ), ); - }); + } return searchResults; } catch (e) { - _logger.severe("Error in getAllLocationTags", e); + _logger.severe("Error in getAllContactSearchResults", e); return []; } } diff --git a/mobile/lib/services/smart_memories_service.dart b/mobile/lib/services/smart_memories_service.dart index 5b66526b36..fecdebeae0 100644 --- a/mobile/lib/services/smart_memories_service.dart +++ b/mobile/lib/services/smart_memories_service.dart @@ -21,6 +21,7 @@ import "package:photos/models/memories/clip_memory.dart"; import "package:photos/models/memories/filler_memory.dart"; import "package:photos/models/memories/memories_cache.dart"; import "package:photos/models/memories/memory.dart"; +import "package:photos/models/memories/on_this_day_memory.dart"; import "package:photos/models/memories/people_memory.dart"; import "package:photos/models/memories/smart_memory.dart"; import "package:photos/models/memories/smart_memory_constants.dart"; @@ -30,6 +31,7 @@ import "package:photos/models/ml/face/face_with_embedding.dart"; import "package:photos/models/ml/face/person.dart"; import "package:photos/models/ml/vector.dart"; import "package:photos/service_locator.dart"; +import "package:photos/services/collections_service.dart"; import "package:photos/services/language_service.dart"; import "package:photos/services/location_service.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; @@ -61,7 +63,7 @@ class SmartMemoriesService { SmartMemoriesService(); // One general method to get all memories, which calls on internal methods for each separate memory type - Future calcMemories( + Future calcSmartMemories( DateTime now, MemoriesCache oldCache, { bool debugSurfaceAll = false, @@ -75,6 +77,11 @@ class SmartMemoriesService { final (allFiles, allFileIdsToFile) = await _getFilesAndMapForMemories(); _logger.finest("All files length: ${allFiles.length} $t"); + final collectionIDsToExclude = await getCollectionIDsToExclude(); + _logger.finest( + 'collectionIDsToExclude length: ${collectionIDsToExclude.length} $t', + ); + final seenTimes = await _memoriesDB.getSeenTimes(); _logger.finest('seenTimes has ${seenTimes.length} entries $t'); @@ -127,6 +134,7 @@ class SmartMemoriesService { param: { "allFiles": allFiles, "allFileIdsToFile": allFileIdsToFile, + "collectionIDsToExclude": collectionIDsToExclude, "now": now, "oldCache": oldCache, "debugSurfaceAll": debugSurfaceAll, @@ -194,6 +202,7 @@ class SmartMemoriesService { // Arguments: direct data final Set allFiles = args["allFiles"]; final Map allFileIdsToFile = args["allFileIdsToFile"]; + final Set collectionIDsToExclude = args["collectionIDsToExclude"]; final DateTime now = args["now"]; final MemoriesCache oldCache = args["oldCache"]; final bool debugSurfaceAll = args["debugSurfaceAll"] ?? false; @@ -230,6 +239,17 @@ class SmartMemoriesService { final List memories = []; + // On this day memories + final onThisDayMemories = await _getOnThisDayResults( + allFiles, + now, + seenTimes: seenTimes, + collectionIDsToExclude: collectionIDsToExclude, + ); + _deductUsedMemories(allFiles, onThisDayMemories); + memories.addAll(onThisDayMemories); + dev.log("All files length after on this day: ${allFiles.length} $t"); + // People memories final peopleMemories = await _getPeopleResults( allFiles, @@ -308,22 +328,41 @@ class SmartMemoriesService { } } - Future> calcFillerResults() async { + Future> calcSimpleMemories() async { final now = DateTime.now(); final (allFiles, _) = await _getFilesAndMapForMemories(); final seenTimes = await _memoriesDB.getSeenTimes(); + final collectionIDsToExclude = await getCollectionIDsToExclude(); + + final List memories = []; + + // On this day memories + final onThisDayMemories = await _getOnThisDayResults( + allFiles, + now, + seenTimes: seenTimes, + collectionIDsToExclude: collectionIDsToExclude, + ); + if (onThisDayMemories.isNotEmpty && + onThisDayMemories.first.shouldShowNow()) { + memories.add(onThisDayMemories.first); + _deductUsedMemories(allFiles, [onThisDayMemories.first]); + } + + // Filler memories final fillerMemories = await _getFillerResults(allFiles, now, seenTimes: seenTimes); + memories.addAll(fillerMemories); final local = await getLocale(); final languageCode = local?.languageCode ?? "en"; final s = await LanguageService.s; _logger.finest('get locale and S'); - for (final memory in fillerMemories) { + for (final memory in memories) { memory.title = memory.createTitle(s, languageCode); } - return fillerMemories; + return memories; } static void _deductUsedMemories( @@ -378,7 +417,7 @@ class SmartMemoriesService { } } final List orderedImportantPersonsID = persons - .where((person) => !person.data.isHidden) + .where((person) => !person.data.isHidden && !person.data.isIgnored) .map((p) => p.remoteID) .toList(); orderedImportantPersonsID.shuffle(Random()); @@ -1542,6 +1581,172 @@ class SmartMemoriesService { return memoryResults; } + Future> getCollectionIDsToExclude() async { + final collections = CollectionsService.instance.getCollectionsForUI(); + + // Names of collections to exclude + const excludedNames = { + 'screenshot', + 'whatsapp', + 'telegram', + 'download', + 'facebook', + 'instagram', + 'messenger', + 'twitter', + 'reddit', + 'discord', + 'signal', + 'viber', + 'wechat', + 'line', + 'meme', + 'internet', + 'saved images', + 'document', + }; + + final excludedCollectionIDs = {}; + collectionLoop: + for (final collection in collections) { + final collectionName = collection.displayName.toLowerCase(); + for (final excludedName in excludedNames) { + if (collectionName.contains(excludedName)) { + excludedCollectionIDs.add(collection.id); + continue collectionLoop; + } + } + } + + return excludedCollectionIDs; + } + + static Future> _getOnThisDayResults( + Iterable allFiles, + DateTime currentTime, { + required Map seenTimes, + required Set collectionIDsToExclude, + }) async { + final List memoryResults = []; + if (allFiles.isEmpty) return []; + + final daysToCompute = kMemoriesUpdateFrequency.inDays; + final currentYear = currentTime.year; + final currentMonth = currentTime.month; + final currentDay = currentTime.day; + final startPoint = DateTime(currentYear, currentMonth, currentDay); + final cutOffTime = startPoint + .subtract(const Duration(days: 363) - kMemoriesUpdateFrequency); + final diffThreshold = Duration(days: daysToCompute); + + final Map> daysToMemories = {}; + final Map> daysToYears = {}; + + final timeTillYearEnd = DateTime(currentYear + 1).difference(startPoint); + final bool almostYearEnd = timeTillYearEnd < diffThreshold; + + // Find all the relevant memories + for (final file in allFiles) { + if (collectionIDsToExclude.contains(file.collectionID)) continue; + if (file.creationTime! > cutOffTime.microsecondsSinceEpoch) { + continue; + } + final fileDate = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + final fileTimeInYear = fileDate.copyWith(year: currentYear); + final diff = fileTimeInYear.difference(startPoint); + if (!diff.isNegative && diff < diffThreshold) { + daysToMemories + .putIfAbsent(diff.inDays, () => []) + .add(Memory.fromFile(file, seenTimes)); + daysToYears.putIfAbsent(diff.inDays, () => []).add(fileDate.year); + } else if (almostYearEnd) { + final altDiff = fileDate.copyWith(year: currentYear + 1).difference( + currentTime, + ); + if (!altDiff.isNegative && altDiff < diffThreshold) { + daysToMemories + .putIfAbsent(altDiff.inDays, () => []) + .add(Memory.fromFile(file, seenTimes)); + daysToYears.putIfAbsent(altDiff.inDays, () => []).add(fileDate.year); + } + } + } + + // Per day, filter the memories to find the best ones + for (var day = 0; day < daysToCompute; day++) { + final memories = daysToMemories[day]; + if (memories == null) continue; + if (memories.length < 5) continue; + final years = daysToYears[day]!; + if (years.toSet().length < 3) continue; + final yearCounts = {}; + for (final year in years) { + yearCounts[year] = (yearCounts[year] ?? 0) + 1; + } + final bool hasThreeInAtLeastThreeYears = + yearCounts.values.where((count) => count >= 3).length >= 3; + if (!hasThreeInAtLeastThreeYears) continue; + + final filteredMemories = []; + if (memories.length > 20) { + // Group memories by year + final Map> memoriesByYear = {}; + for (final memory in memories) { + final creationTime = + DateTime.fromMicrosecondsSinceEpoch(memory.file.creationTime!); + final year = creationTime.year; + memoriesByYear.putIfAbsent(year, () => []).add(memory); + } + for (final year in memoriesByYear.keys) { + memoriesByYear[year]!.shuffle(Random()); + } + + // Get all years, randonly select 20 years if there are more than 20 + List years = memoriesByYear.keys.toList()..sort(); + if (years.length > 20) { + years.shuffle(Random()); + years = years.take(20).toList()..sort(); + } + + // First round to take one memory from each year + for (final year in years) { + if (filteredMemories.length >= 20) break; + final yearMemories = memoriesByYear[year]!; + if (yearMemories.isNotEmpty) { + filteredMemories.add(yearMemories.removeAt(0)); + } + } + + // Second round to fill up to 20 memories + while (filteredMemories.length < 20) { + bool addedAny = false; + for (final year in years) { + if (filteredMemories.length >= 20) break; + final yearMemories = memoriesByYear[year]!; + if (yearMemories.isNotEmpty) { + filteredMemories.add(yearMemories.removeAt(0)); + addedAny = true; + } + } + if (!addedAny) break; + } + } else { + filteredMemories.addAll(memories); + } + + filteredMemories.sort( + (a, b) => a.file.creationTime!.compareTo(b.file.creationTime!), + ); + final onThisDayMemory = OnThisDayMemory( + filteredMemories, + startPoint.add(Duration(days: day)).microsecondsSinceEpoch, + startPoint.add(Duration(days: day + 1)).microsecondsSinceEpoch, + ); + memoryResults.add(onThisDayMemory); + } + return memoryResults; + } + static Future getDateFormattedLocale({ required int creationTime, }) async { diff --git a/mobile/lib/services/sync/remote_sync_service.dart b/mobile/lib/services/sync/remote_sync_service.dart index bedbc773d4..61000fdc0b 100644 --- a/mobile/lib/services/sync/remote_sync_service.dart +++ b/mobile/lib/services/sync/remote_sync_service.dart @@ -25,8 +25,8 @@ import 'package:photos/models/upload_strategy.dart'; import "package:photos/service_locator.dart"; import 'package:photos/services/app_lifecycle_service.dart'; import 'package:photos/services/collections_service.dart'; -import "package:photos/services/filedata/filedata_service.dart"; import 'package:photos/services/ignored_files_service.dart'; +import "package:photos/services/language_service.dart"; import 'package:photos/services/local_file_update_service.dart'; import "package:photos/services/notification_service.dart"; import "package:photos/services/preview_video_store.dart"; @@ -127,7 +127,7 @@ class RemoteSyncService { await syncDeviceCollectionFilesForUpload(); } - FileDataService.instance.syncFDStatus().then((_) { + fileDataService.syncFDStatus().then((_) { PreviewVideoStore.instance.queueFiles(); }).ignore(); final filesToBeUploaded = await _getFilesToBeUploaded(); @@ -572,7 +572,7 @@ class RemoteSyncService { _logger.info("Skipped $skippedVideos videos and $ignoredForUpload " "ignored files for upload"); } - _sortByTimeAndType(filesToBeUploaded); + _sortByTime(filesToBeUploaded); _logger.info("${filesToBeUploaded.length} new files to be uploaded."); return filesToBeUploaded; } @@ -933,17 +933,11 @@ class RemoteSyncService { return Platform.isIOS && !AppLifecycleService.instance.isForeground; } - // _sortByTimeAndType moves videos to end and sort by creation time (desc). + // _sortByTime sort by creation time (desc). // This is done to upload most recent photo first. - void _sortByTimeAndType(List file) { + void _sortByTime(List file) { file.sort((first, second) { - if (first.fileType == second.fileType) { - return second.creationTime!.compareTo(first.creationTime!); - } else if (first.fileType == FileType.video) { - return 1; - } else { - return -1; - } + return second.creationTime!.compareTo(first.creationTime!); }); // move updated files towards the end file.sort((first, second) { @@ -998,10 +992,11 @@ class RemoteSyncService { 'creating notification for ${collection?.displayName} ' 'shared: $sharedFilesIDs, collected: $collectedFilesIDs files', ); + final s = await LanguageService.s; // ignore: unawaited_futures NotificationService.instance.showNotification( collection!.displayName, - totalCount.toString() + " new 📸", + totalCount.toString() + s.newPhotosEmoji, channelID: "collection:" + collectionID.toString(), channelName: collection.displayName, payload: "ente://collection/?collectionID=" + collectionID.toString(), diff --git a/mobile/lib/services/sync/sync_service.dart b/mobile/lib/services/sync/sync_service.dart index 16bb94fa05..ac1ee5bb45 100644 --- a/mobile/lib/services/sync/sync_service.dart +++ b/mobile/lib/services/sync/sync_service.dart @@ -13,6 +13,7 @@ import 'package:photos/events/subscription_purchased_event.dart'; import 'package:photos/events/sync_status_update_event.dart'; import 'package:photos/events/trigger_logout_event.dart'; import 'package:photos/models/file/file_type.dart'; +import "package:photos/services/language_service.dart"; import 'package:photos/services/notification_service.dart'; import 'package:photos/services/sync/local_sync_service.dart'; import 'package:photos/services/sync/remote_sync_service.dart'; @@ -207,10 +208,11 @@ class SyncService { final now = DateTime.now().microsecondsSinceEpoch; if ((now - lastNotificationShownTime) > microSecondsInDay) { await _prefs.setInt(kLastStorageLimitExceededNotificationPushTime, now); + final s = await LanguageService.s; // ignore: unawaited_futures NotificationService.instance.showNotification( - "Storage limit exceeded", - "Sorry, we had to pause your backups", + s.storageLimitExceeded, + s.sorryWeHadToPauseYourBackups, ); } } diff --git a/mobile/lib/services/update_service.dart b/mobile/lib/services/update_service.dart index 4b73cbad6c..10b5589ebd 100644 --- a/mobile/lib/services/update_service.dart +++ b/mobile/lib/services/update_service.dart @@ -5,6 +5,7 @@ import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/core/network/network.dart'; +import "package:photos/services/language_service.dart"; import 'package:photos/services/notification_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tuple/tuple.dart'; @@ -13,7 +14,7 @@ import 'package:url_launcher/url_launcher_string.dart'; class UpdateService { static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key"; static const changeLogVersionKey = "update_change_log_key"; - static const currentChangeLogVersion = 27; + static const currentChangeLogVersion = 28; LatestVersionInfo? _latestVersion; final _logger = Logger("UpdateService"); @@ -90,10 +91,11 @@ class UpdateService { Future showUpdateNotification() async { if (await shouldShowUpdateNotification()) { + final s = await LanguageService.s; // ignore: unawaited_futures NotificationService.instance.showNotification( - "Update available", - "Click to install our best version yet", + s.updateAvailable, + s.clickToInstallOurBestVersionYet, ); await resetUpdateAvailableShownTime(); } else { diff --git a/mobile/lib/states/all_sections_examples_state.dart b/mobile/lib/states/all_sections_examples_state.dart index 19e640d318..2a9482f63f 100644 --- a/mobile/lib/states/all_sections_examples_state.dart +++ b/mobile/lib/states/all_sections_examples_state.dart @@ -96,9 +96,16 @@ class _AllSectionsExamplesProviderState _logger.info("'_debounceTimer: reloading all sections in search tab"); final allSectionsExamples = >>[]; for (SectionType sectionType in SectionType.values) { - allSectionsExamples.add( - sectionType.getData(context, limit: kSearchSectionLimit), - ); + // Contacts section have been moved to shared collections tab + // temporarily from search tab. So we can skip computing data here + // since 'allSectionsExamples' is for search tab sections only. + if (sectionType == SectionType.contacts) { + allSectionsExamples.add(Future.value([])); + } else { + allSectionsExamples.add( + sectionType.getData(context, limit: kSearchSectionLimit), + ); + } } try { allSectionsExamplesFuture = Future.wait>( diff --git a/mobile/lib/ui/account/change_email_dialog.dart b/mobile/lib/ui/account/change_email_dialog.dart index 3f0c83d9c1..974ad17a6f 100644 --- a/mobile/lib/ui/account/change_email_dialog.dart +++ b/mobile/lib/ui/account/change_email_dialog.dart @@ -18,7 +18,7 @@ class _ChangeEmailDialogState extends State { Widget build(BuildContext context) { final l10n = context.l10n; return AlertDialog( - title: Text(l10n.enterYourEmailAddress), + title: Text(l10n.enterYourNewEmailAddress), content: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, diff --git a/mobile/lib/ui/actions/collection/collection_file_actions.dart b/mobile/lib/ui/actions/collection/collection_file_actions.dart index cf28e6687c..2deb10cf2a 100644 --- a/mobile/lib/ui/actions/collection/collection_file_actions.dart +++ b/mobile/lib/ui/actions/collection/collection_file_actions.dart @@ -84,6 +84,104 @@ extension CollectionFileActions on CollectionActions { } } + Future addToMultipleCollections( + BuildContext context, + List collections, + bool showProgressDialog, { + List? selectedFiles, + }) async { + final ProgressDialog? dialog = showProgressDialog + ? createProgressDialog( + context, + S.of(context).uploadingFilesToAlbum, + isDismissible: true, + ) + : null; + await dialog?.show(); + final int currentUserID = Configuration.instance.getUserID()!; + for (final collection in collections) { + try { + final List files = []; + final List filesPendingUpload = []; + for (final file in selectedFiles!) { + EnteFile? currentFile; + if (file.uploadedFileID != null) { + currentFile = file.copyWith(); + } else if (file.generatedID != null) { + // when file is not uploaded, refresh the state from the db to + // ensure we have latest upload status for given file before + // queueing it up as pending upload + currentFile = await (FilesDB.instance.getFile(file.generatedID!)); + } else if (file.generatedID == null) { + logger.severe("generated id should not be null"); + } + if (currentFile == null) { + logger.severe("Failed to find fileBy genID"); + continue; + } + + if (currentFile.uploadedFileID == null) { + currentFile.collectionID = collection.id; + filesPendingUpload.add(currentFile); + } else { + files.add(currentFile); + } + } + if (filesPendingUpload.isNotEmpty) { + // Newly created collection might not be cached + final Collection? c = + CollectionsService.instance.getCollectionByID(collection.id); + if (c != null && c.owner.id != currentUserID) { + final Collection uncat = + await CollectionsService.instance.getUncategorizedCollection(); + for (EnteFile unuploadedFile in filesPendingUpload) { + final uploadedFile = await FileUploader.instance.forceUpload( + unuploadedFile, + uncat.id, + ); + files.add(uploadedFile); + } + } else { + for (final file in filesPendingUpload) { + file.collectionID = collection.id; + } + // filesPendingUpload might be getting ignored during auto-upload + // because the user deleted these files from ente in the past. + await IgnoredFilesService.instance + .removeIgnoredMappings(filesPendingUpload); + await FilesDB.instance.insertMultiple(filesPendingUpload); + Bus.instance.fire( + CollectionUpdatedEvent( + collection.id, + filesPendingUpload, + "pendingFilesAdd", + ), + ); + } + } + if (files.isNotEmpty) { + await CollectionsService.instance + .addOrCopyToCollection(collection.id, files); + } + } catch (e, s) { + logger.severe("Failed to add to album", e, s); + await dialog?.hide(); + await showGenericErrorDialog( + context: context, + error: e, + ); + return false; + } finally { + // Syncing since successful addition to collection could have + // happened before a failure + unawaited(RemoteSyncService.instance.sync(silently: true)); + } + } + + await dialog?.hide(); + return true; + } + Future addToCollection( BuildContext context, int collectionID, diff --git a/mobile/lib/ui/actions/collection/collection_sharing_actions.dart b/mobile/lib/ui/actions/collection/collection_sharing_actions.dart index 87b552269d..0583798dcf 100644 --- a/mobile/lib/ui/actions/collection/collection_sharing_actions.dart +++ b/mobile/lib/ui/actions/collection/collection_sharing_actions.dart @@ -333,6 +333,90 @@ class CollectionActions { } } + Future deleteMultipleCollectionSheet( + BuildContext bContext, + List collections, + ) async { + final textTheme = getEnteTextTheme(bContext); + final actionResult = await showActionSheet( + context: bContext, + buttons: [ + ButtonWidget( + labelText: S.of(bContext).keepPhotos, + buttonType: ButtonType.neutral, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.first, + shouldStickToDarkTheme: true, + isInAlert: true, + onTap: () async { + for (final collection in collections) { + try { + await trashCollectionKeepingPhotos(collection, bContext); + } catch (e, s) { + logger.severe( + "Failed to keep photos & delete collection", + e, + s, + ); + rethrow; + } + } + }, + ), + ButtonWidget( + labelText: S.of(bContext).deletePhotos, + buttonType: ButtonType.critical, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.second, + shouldStickToDarkTheme: true, + isInAlert: true, + onTap: () async { + for (final collection in collections) { + try { + await collectionsService.trashNonEmptyCollection(collection); + } catch (e) { + logger.severe("Failed to delete collection", e); + rethrow; + } + } + }, + ), + ButtonWidget( + labelText: S.of(bContext).cancel, + buttonType: ButtonType.secondary, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.third, + shouldStickToDarkTheme: true, + isInAlert: true, + ), + ], + bodyWidget: StyledText( + text: S.of(bContext).deleteMultipleAlbumDialog(collections.length), + style: textTheme.body.copyWith(color: textMutedDark), + tags: { + 'bold': StyledTextTag( + style: textTheme.body.copyWith(color: textBaseDark), + ), + }, + ), + actionSheetType: ActionSheetType.defaultActionSheet, + ); + if (actionResult?.action != null && + actionResult!.action == ButtonAction.error) { + await showGenericErrorDialog( + context: bContext, + error: actionResult.exception, + ); + return false; + } + if ((actionResult?.action != null) && + (actionResult!.action == ButtonAction.first || + actionResult.action == ButtonAction.second)) { + return true; + } + return false; + } + // deleteCollectionSheet returns true if the album is successfully deleted Future deleteCollectionSheet( BuildContext bContext, diff --git a/mobile/lib/ui/collections/album/column_item.dart b/mobile/lib/ui/collections/album/column_item.dart index e3d62b01a3..bf793d7214 100644 --- a/mobile/lib/ui/collections/album/column_item.dart +++ b/mobile/lib/ui/collections/album/column_item.dart @@ -6,16 +6,19 @@ import 'package:photos/models/collection/collection.dart'; import 'package:photos/models/file/file.dart'; import "package:photos/services/collections_service.dart"; import 'package:photos/theme/ente_theme.dart'; +import "package:photos/ui/components/buttons/icon_button_widget.dart"; import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart'; import 'package:photos/ui/viewer/file/thumbnail_widget.dart'; ///https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=7480%3A33462&t=H5AvR79OYDnB9ekw-4 class AlbumColumnItemWidget extends StatelessWidget { final Collection collection; + final List selectedCollections; const AlbumColumnItemWidget( this.collection, { super.key, + this.selectedCollections = const [], }); @override @@ -23,12 +26,25 @@ class AlbumColumnItemWidget extends StatelessWidget { final textTheme = getEnteTextTheme(context); final colorScheme = getEnteColorScheme(context); const sideOfThumbnail = 60.0; - return LayoutBuilder( - builder: (context, constraints) { - return Stack( - alignment: Alignment.center, - children: [ - Row( + final isSelected = selectedCollections.contains(collection); + return AnimatedContainer( + curve: Curves.easeOut, + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + border: Border.all( + color: + isSelected ? colorScheme.strokeMuted : colorScheme.strokeFainter, + ), + borderRadius: const BorderRadius.all( + Radius.circular(4), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + flex: 6, + child: Row( children: [ ClipRRect( borderRadius: const BorderRadius.horizontal( @@ -57,62 +73,72 @@ class AlbumColumnItemWidget extends StatelessWidget { ), ), ), - Padding( - padding: const EdgeInsets.only(left: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(collection.displayName), - FutureBuilder( - future: CollectionsService.instance - .getFileCount(collection), - builder: (context, snapshot) { - if (snapshot.hasData) { - return Text( - S.of(context).memoryCount( - snapshot.data!, - NumberFormat().format(snapshot.data!), - ), - style: textTheme.small.copyWith( - color: colorScheme.textMuted, - ), - ); - } else { - if (snapshot.hasError) { - Logger("AlbumListItemWidget").severe( - "Failed to fetch file count of collection", - snapshot.error, + const SizedBox(width: 12), + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + collection.displayName, + overflow: TextOverflow.ellipsis, + ), + FutureBuilder( + future: CollectionsService.instance + .getFileCount(collection), + builder: (context, snapshot) { + if (snapshot.hasData) { + return Text( + S.of(context).memoryCount( + snapshot.data!, + NumberFormat().format(snapshot.data!), + ), + style: textTheme.small.copyWith( + color: colorScheme.textMuted, + ), + ); + } else { + if (snapshot.hasError) { + Logger("AlbumListItemWidget").severe( + "Failed to fetch file count of collection", + snapshot.error, + ); + } + return Text( + "", + style: textTheme.small.copyWith( + color: colorScheme.textMuted, + ), ); } - return Text( - "", - style: textTheme.small.copyWith( - color: colorScheme.textMuted, - ), - ); - } - }, - ), - ], + }, + ), + ], + ), ), ), ], ), - IgnorePointer( - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(4)), - border: Border.all( - color: colorScheme.strokeFainter, - ), - ), - height: sideOfThumbnail, - width: constraints.maxWidth, - ), + ), + Flexible( + flex: 1, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + child: isSelected + ? IconButtonWidget( + key: const ValueKey("selected"), + icon: Icons.check_circle_rounded, + iconButtonType: IconButtonType.secondary, + iconColor: colorScheme.blurStrokeBase, + ) + : null, ), - ], - ); - }, + ), + ], + ), ); } } diff --git a/mobile/lib/ui/collections/album/list_item.dart b/mobile/lib/ui/collections/album/list_item.dart new file mode 100644 index 0000000000..255d6de147 --- /dev/null +++ b/mobile/lib/ui/collections/album/list_item.dart @@ -0,0 +1,160 @@ +import "package:flutter/material.dart"; +import "package:intl/intl.dart"; +import "package:logging/logging.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/models/collection/collection.dart"; +import "package:photos/models/file/file.dart"; +import "package:photos/models/selected_albums.dart"; +import "package:photos/services/collections_service.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/components/buttons/icon_button_widget.dart"; +import "package:photos/ui/viewer/file/no_thumbnail_widget.dart"; +import "package:photos/ui/viewer/file/thumbnail_widget.dart"; + +class AlbumListItemWidget extends StatelessWidget { + final Collection collection; + final void Function(Collection)? onTapCallback; + final void Function(Collection)? onLongPressCallback; + final SelectedAlbums? selectedAlbums; + + const AlbumListItemWidget( + this.collection, { + super.key, + this.onTapCallback, + this.onLongPressCallback, + this.selectedAlbums, + }); + + @override + Widget build(BuildContext context) { + final textTheme = getEnteTextTheme(context); + final colorScheme = getEnteColorScheme(context); + const sideOfThumbnail = 60.0; + + final albumWidget = Flexible( + flex: 6, + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(4), + ), + child: SizedBox( + height: sideOfThumbnail, + width: sideOfThumbnail, + child: FutureBuilder( + future: CollectionsService.instance.getCover(collection), + builder: (context, snapshot) { + if (snapshot.hasData) { + final thumbnail = snapshot.data!; + return ThumbnailWidget( + thumbnail, + showFavForAlbumOnly: true, + shouldShowOwnerAvatar: false, + ); + } else { + return const NoThumbnailWidget(addBorder: false); + } + }, + ), + ), + ), + const SizedBox(width: 12), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + collection.displayName, + overflow: TextOverflow.ellipsis, + ), + FutureBuilder( + future: CollectionsService.instance.getFileCount(collection), + builder: (context, snapshot) { + if (snapshot.hasData) { + return Text( + S.of(context).memoryCount( + snapshot.data!, + NumberFormat().format(snapshot.data!), + ), + style: textTheme.small.copyWith( + color: colorScheme.textMuted, + ), + ); + } else { + if (snapshot.hasError) { + Logger("AlbumListItemWidget").severe( + "Failed to fetch file count of collection", + snapshot.error, + ); + } + return Text( + "", + style: textTheme.small.copyWith( + color: colorScheme.textMuted, + ), + ); + } + }, + ), + ], + ), + ), + ], + ), + ); + + return GestureDetector( + onTap: () => onTapCallback?.call(collection), + onLongPress: () => onLongPressCallback?.call(collection), + behavior: HitTestBehavior.opaque, + child: ListenableBuilder( + listenable: selectedAlbums!, + builder: (context, _) { + final isSelected = + selectedAlbums?.isAlbumSelected(collection) ?? false; + + return AnimatedContainer( + curve: Curves.easeOut, + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + border: Border.all( + color: isSelected + ? colorScheme.strokeMuted + : colorScheme.strokeFainter, + ), + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + albumWidget, + Flexible( + flex: 1, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + child: isSelected + ? IconButtonWidget( + key: const ValueKey("selected"), + icon: Icons.check_circle_rounded, + iconButtonType: IconButtonType.secondary, + iconColor: colorScheme.blurStrokeBase, + ) + : IconButtonWidget( + key: const ValueKey("unselected"), + icon: Icons.chevron_right_outlined, + iconButtonType: IconButtonType.secondary, + iconColor: colorScheme.blurStrokePressed, + ), + ), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/mobile/lib/ui/collections/album/new_list_item.dart b/mobile/lib/ui/collections/album/new_list_item.dart index 4274fafcc8..735946f223 100644 --- a/mobile/lib/ui/collections/album/new_list_item.dart +++ b/mobile/lib/ui/collections/album/new_list_item.dart @@ -47,7 +47,7 @@ class NewAlbumListItemWidget extends StatelessWidget { IgnorePointer( child: DottedBorder( dashPattern: const [4], - color: colorScheme.strokeFainter, + color: colorScheme.strokeFaint, strokeWidth: 1, padding: const EdgeInsets.all(0), borderType: BorderType.RRect, diff --git a/mobile/lib/ui/collections/new_album_icon.dart b/mobile/lib/ui/collections/album/new_row_item.dart similarity index 56% rename from mobile/lib/ui/collections/new_album_icon.dart rename to mobile/lib/ui/collections/album/new_row_item.dart index 4e18123b66..3e000cd12d 100644 --- a/mobile/lib/ui/collections/new_album_icon.dart +++ b/mobile/lib/ui/collections/album/new_row_item.dart @@ -1,30 +1,29 @@ +import "package:dotted_border/dotted_border.dart"; import 'package:flutter/material.dart'; import "package:logging/logging.dart"; import "package:photos/generated/l10n.dart"; import 'package:photos/models/collection/collection.dart'; import 'package:photos/models/collection/collection_items.dart'; import "package:photos/services/collections_service.dart"; -import "package:photos/ui/components/buttons/icon_button_widget.dart"; +import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/viewer/gallery/collection_page.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/navigation_util.dart"; -class NewAlbumIcon extends StatelessWidget { - final IconData icon; +class NewAlbumRowItemWidget extends StatelessWidget { final Color? color; - final IconButtonType iconButtonType; - const NewAlbumIcon({ - required this.icon, - required this.iconButtonType, + final double height; + final double width; + const NewAlbumRowItemWidget({ this.color, super.key, + required this.height, + required this.width, }); @override Widget build(BuildContext context) { - return IconButtonWidget( - icon: icon, - iconButtonType: iconButtonType, + return GestureDetector( onTap: () async { final result = await showTextInputDialog( context, @@ -34,7 +33,7 @@ class NewAlbumIcon extends StatelessWidget { alwaysShowSuccessState: false, initialValue: "", textCapitalization: TextCapitalization.words, - popnavAfterSubmission: false, + popnavAfterSubmission: true, onSubmit: (String text) async { if (text.trim() == "") { return; @@ -48,9 +47,8 @@ class NewAlbumIcon extends StatelessWidget { context, CollectionPage(CollectionWithThumbnail(c, null)), ); - Navigator.of(context).pop(); } catch (e, s) { - Logger("CreateNewAlbumIcon") + Logger("CreateNewAlbumRowItemWidget") .severe("Failed to rename album", e, s); rethrow; } @@ -61,6 +59,34 @@ class NewAlbumIcon extends StatelessWidget { await showGenericErrorDialog(context: context, error: result); } }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: height, + width: width, + child: DottedBorder( + borderType: BorderType.RRect, + strokeWidth: 1.5, + dashPattern: const [3.75, 3.75], + radius: const Radius.circular(2.35), + padding: EdgeInsets.zero, + color: getEnteColorScheme(context).strokeMuted, + child: Center( + child: Icon( + Icons.add, + color: getEnteColorScheme(context).strokeMuted, + ), + ), + ), + ), + const SizedBox(height: 4), + Text( + S.of(context).addNew, + style: getEnteTextTheme(context).smallFaint, + ), + ], + ), ); } } diff --git a/mobile/lib/ui/collections/album/row_item.dart b/mobile/lib/ui/collections/album/row_item.dart index 5f684d3780..5467a27a53 100644 --- a/mobile/lib/ui/collections/album/row_item.dart +++ b/mobile/lib/ui/collections/album/row_item.dart @@ -4,6 +4,7 @@ import "package:photos/core/configuration.dart"; import 'package:photos/models/collection/collection.dart'; import 'package:photos/models/collection/collection_items.dart'; import 'package:photos/models/file/file.dart'; +import "package:photos/models/selected_albums.dart"; import "package:photos/services/collections_service.dart"; import "package:photos/theme/colors.dart"; import 'package:photos/theme/ente_theme.dart'; @@ -20,6 +21,9 @@ class AlbumRowItemWidget extends StatelessWidget { final bool showFileCount; final String tag; final bool? hasVerifiedLock; + final void Function(Collection)? onTapCallback; + final void Function(Collection)? onLongPressCallback; + final SelectedAlbums? selectedAlbums; const AlbumRowItemWidget( this.c, @@ -28,6 +32,9 @@ class AlbumRowItemWidget extends StatelessWidget { this.showFileCount = true, this.tag = "", this.hasVerifiedLock, + this.onTapCallback, + this.onLongPressCallback, + this.selectedAlbums, }); @override @@ -44,6 +51,7 @@ class AlbumRowItemWidget extends StatelessWidget { color: c.publicURLs.first.isExpired ? warning500 : strokeBaseDark, ) : null; + return GestureDetector( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -70,20 +78,33 @@ class AlbumRowItemWidget extends StatelessWidget { CollectionsService.instance.getCoverCache(c); } if (thumbnail != null) { + final bool isSelected = + selectedAlbums?.isAlbumSelected(c) ?? false; final String heroTag = tagPrefix + thumbnail.tag; + final thumbnailWidget = ThumbnailWidget( + thumbnail, + shouldShowArchiveStatus: isOwner + ? c.isArchived() + : c.hasShareeArchived(), + showFavForAlbumOnly: true, + shouldShowSyncStatus: false, + shouldShowPinIcon: isOwner && c.isPinned, + key: Key(heroTag), + ); return Hero( tag: heroTag, transitionOnUserGestures: true, - child: ThumbnailWidget( - thumbnail, - shouldShowArchiveStatus: isOwner - ? c.isArchived() - : c.hasShareeArchived(), - showFavForAlbumOnly: true, - shouldShowSyncStatus: false, - shouldShowPinIcon: isOwner && c.isPinned, - key: Key(heroTag), - ), + child: isSelected + ? ColorFiltered( + colorFilter: ColorFilter.mode( + Colors.black.withOpacity( + 0.4, + ), + BlendMode.darken, + ), + child: thumbnailWidget, + ) + : thumbnailWidget, ); } else { return const NoThumbnailWidget(); @@ -97,12 +118,40 @@ class AlbumRowItemWidget extends StatelessWidget { child: Align( alignment: Alignment.topLeft, child: AlbumSharesIcons( + padding: const EdgeInsets.only(left: 4, top: 4), sharees: c.getSharees(), type: AvatarType.mini, trailingWidget: linkIcon, ), ), ), + Positioned( + top: 5, + right: 5, + child: Hero( + tag: tagPrefix + "_album_selection", + transitionOnUserGestures: true, + child: ListenableBuilder( + listenable: selectedAlbums ?? ValueNotifier(false), + builder: (context, _) { + final bool isSelected = + selectedAlbums?.isAlbumSelected(c) ?? false; + return AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + child: isSelected + ? const Icon( + Icons.check_circle_rounded, + color: Colors.white, + size: 22, + ) + : null, + ); + }, + ), + ), + ), if (!isOwner) Align( alignment: Alignment.bottomRight, @@ -110,10 +159,8 @@ class AlbumRowItemWidget extends StatelessWidget { tag: tagPrefix + "_owner_other", transitionOnUserGestures: true, child: Padding( - padding: const EdgeInsets.only( - right: 8.0, - bottom: 8.0, - ), + padding: + const EdgeInsets.only(right: 4, bottom: 4), child: UserAvatarWidget( c.owner, thumbnailView: true, @@ -151,12 +198,12 @@ class AlbumRowItemWidget extends StatelessWidget { } if (cachedCount != null && cachedCount > 0) { final String textCount = NumberFormat().format(cachedCount); - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( constraints: BoxConstraints( - maxWidth: - sideOfThumbnail - ((textCount.length + 3) * 10), + maxWidth: sideOfThumbnail, ), child: Text( c.displayName, @@ -164,11 +211,12 @@ class AlbumRowItemWidget extends StatelessWidget { overflow: TextOverflow.ellipsis, ), ), + const SizedBox(height: 2), RichText( text: TextSpan( - style: enteTextTheme.smallMuted, + style: enteTextTheme.tinyMuted, children: [ - TextSpan(text: ' \u2022 $textCount'), + TextSpan(text: textCount), ], ), ), @@ -188,6 +236,10 @@ class AlbumRowItemWidget extends StatelessWidget { ], ), onTap: () async { + if (onTapCallback != null) { + onTapCallback!(c); + return; + } final thumbnail = await CollectionsService.instance.getCover(c); // ignore: unawaited_futures routeToPage( @@ -199,6 +251,11 @@ class AlbumRowItemWidget extends StatelessWidget { ), ); }, + onLongPress: () { + if (onLongPressCallback != null) { + onLongPressCallback!(c); + } + }, ); } } diff --git a/mobile/lib/ui/collections/album/vertical_list.dart b/mobile/lib/ui/collections/album/vertical_list.dart index 78bcdc8b2f..d0f07e3c7c 100644 --- a/mobile/lib/ui/collections/album/vertical_list.dart +++ b/mobile/lib/ui/collections/album/vertical_list.dart @@ -1,6 +1,7 @@ import "dart:async"; import 'package:flutter/material.dart'; +import "package:flutter/services.dart"; import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; import "package:photos/core/event_bus.dart"; @@ -25,63 +26,96 @@ import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/navigation_util.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; -class AlbumVerticalListWidget extends StatelessWidget { +class AlbumVerticalListWidget extends StatefulWidget { final List collections; final CollectionActionType actionType; final SelectedFiles? selectedFiles; final List? sharedFiles; final String searchQuery; final bool shouldShowCreateAlbum; + final bool enableSelection; + final List selectedCollections; + final Function()? onSelectionChanged; - AlbumVerticalListWidget( + const AlbumVerticalListWidget( this.collections, this.actionType, this.selectedFiles, this.sharedFiles, this.searchQuery, this.shouldShowCreateAlbum, { + required this.selectedCollections, + this.enableSelection = false, + this.onSelectionChanged, super.key, }); + @override + State createState() => + _AlbumVerticalListWidgetState(); +} + +class _AlbumVerticalListWidgetState extends State { final _logger = Logger("CollectionsListWidgetState"); + final CollectionActions _collectionActions = CollectionActions(CollectionsService.instance); @override Widget build(BuildContext context) { - final filesCount = sharedFiles != null - ? sharedFiles!.length - : selectedFiles?.files.length ?? 0; + final filesCount = widget.sharedFiles != null + ? widget.sharedFiles!.length + : widget.selectedFiles?.files.length ?? 0; - if (collections.isEmpty) { - if (shouldShowCreateAlbum) { + if (widget.collections.isEmpty) { + if (widget.shouldShowCreateAlbum) { return _getNewAlbumWidget(context, filesCount); } return const EmptyState(); } return ListView.separated( itemBuilder: (context, index) { - if (index == 0 && shouldShowCreateAlbum) { + if (index == 0 && widget.shouldShowCreateAlbum) { return _getNewAlbumWidget(context, filesCount); } - final item = collections[index - (shouldShowCreateAlbum ? 1 : 0)]; + final item = + widget.collections[index - (widget.shouldShowCreateAlbum ? 1 : 0)]; return GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => _albumListItemOnTap(context, item), + onTap: () => widget.enableSelection + ? _toggleCollectionSelection(item) + : _albumListItemOnTap(context, item), child: AlbumColumnItemWidget( item, + selectedCollections: widget.selectedCollections, ), ); }, - separatorBuilder: (context, index) => const SizedBox( - height: 8, - ), - itemCount: collections.length + (shouldShowCreateAlbum ? 1 : 0), - shrinkWrap: true, + separatorBuilder: (context, index) => const SizedBox(height: 8), + itemCount: + widget.collections.length + (widget.shouldShowCreateAlbum ? 1 : 0), + shrinkWrap: false, physics: const BouncingScrollPhysics(), ); } + Future _toggleCollectionSelection( + Collection collection, + ) async { + if (widget.selectedCollections.contains(collection)) { + widget.selectedCollections.remove(collection); + } else { + widget.selectedCollections.isEmpty + ? await HapticFeedback.vibrate() + : null; + widget.selectedCollections.add(collection); + } + if (widget.onSelectionChanged != null) { + widget.onSelectionChanged!(); + } + setState(() {}); + } + GestureDetector _getNewAlbumWidget(BuildContext context, int filesCount) { return GestureDetector( onTap: () async { @@ -139,8 +173,8 @@ class AlbumVerticalListWidget extends StatelessWidget { bool hasVerifiedLock = false; late final Collection? collection; - if (actionType == CollectionActionType.moveToHiddenCollection || - actionType == CollectionActionType.addToHiddenAlbum) { + if (widget.actionType == CollectionActionType.moveToHiddenCollection || + widget.actionType == CollectionActionType.addToHiddenAlbum) { collection = await CollectionsService.instance.createHiddenAlbum(albumName); hasVerifiedLock = true; @@ -154,7 +188,7 @@ class AlbumVerticalListWidget extends StatelessWidget { collection, showProgressDialog: false, )) { - if (actionType == CollectionActionType.restoreFiles) { + if (widget.actionType == CollectionActionType.restoreFiles) { showShortToast( context, 'Restored files to album ' + albumName, @@ -199,19 +233,20 @@ class AlbumVerticalListWidget extends StatelessWidget { bool shouldNavigateToCollection = false; bool hasVerifiedLock = false; - if (actionType == CollectionActionType.addFiles) { + if (widget.actionType == CollectionActionType.addFiles) { toastMessage = S.of(context).addedSuccessfullyTo(item.displayName); shouldNavigateToCollection = true; - } else if (actionType == CollectionActionType.moveFiles || - actionType == CollectionActionType.restoreFiles || - actionType == CollectionActionType.unHide) { + } else if (widget.actionType == CollectionActionType.moveFiles || + widget.actionType == CollectionActionType.restoreFiles || + widget.actionType == CollectionActionType.unHide) { toastMessage = S.of(context).movedSuccessfullyTo(item.displayName); shouldNavigateToCollection = true; - } else if (actionType == CollectionActionType.moveToHiddenCollection) { + } else if (widget.actionType == + CollectionActionType.moveToHiddenCollection) { toastMessage = S.of(context).movedSuccessfullyTo(item.displayName); shouldNavigateToCollection = true; hasVerifiedLock = true; - } else if (actionType == CollectionActionType.addToHiddenAlbum) { + } else if (widget.actionType == CollectionActionType.addToHiddenAlbum) { toastMessage = S.of(context).addedSuccessfullyTo(item.displayName); shouldNavigateToCollection = true; hasVerifiedLock = true; @@ -226,7 +261,6 @@ class AlbumVerticalListWidget extends StatelessWidget { } if (shouldNavigateToCollection) { Navigator.pop(context); - await _navigateToCollection( context, item, @@ -241,7 +275,7 @@ class AlbumVerticalListWidget extends StatelessWidget { Collection collection, { bool showProgressDialog = true, }) async { - switch (actionType) { + switch (widget.actionType) { case CollectionActionType.addFiles: return _addToCollection( context, @@ -256,8 +290,6 @@ class AlbumVerticalListWidget extends StatelessWidget { return _restoreFilesToCollection(context, collection.id); case CollectionActionType.shareCollection: return _showShareCollectionPage(context, collection); - case CollectionActionType.collectPhotos: - return _createCollaborativeLink(context, collection); case CollectionActionType.moveToHiddenCollection: return _moveFilesToCollection(context, collection.id); case CollectionActionType.addToHiddenAlbum: @@ -279,76 +311,6 @@ class AlbumVerticalListWidget extends StatelessWidget { ); } - Future _createCollaborativeLink( - BuildContext context, - Collection collection, - ) async { - final CollectionActions collectionActions = - CollectionActions(CollectionsService.instance); - - if (collection.hasLink) { - if (collection.publicURLs.first.enableCollect) { - if (Configuration.instance.getUserID() == collection.owner.id) { - unawaited( - routeToPage( - context, - ShareCollectionPage(collection), - ), - ); - } - showShortToast( - context, - S.of(context).thisAlbumAlreadyHDACollaborativeLink, - ); - return Future.value(false); - } else { - try { - unawaited( - routeToPage( - context, - ShareCollectionPage(collection), - ), - ); - // ignore: unawaited_futures - CollectionsService.instance - .updateShareUrl(collection, {'enableCollect': true}).then( - (value) => showShortToast( - context, - S.of(context).collaborativeLinkCreatedFor(collection.displayName), - ), - ); - return true; - } catch (e) { - await showGenericErrorDialog(context: context, error: e); - return false; - } - } - } - final bool result = await collectionActions.enableUrl( - context, - collection, - enableCollect: true, - ); - if (result) { - showShortToast( - context, - S.of(context).collaborativeLinkCreatedFor(collection.displayName), - ); - if (Configuration.instance.getUserID() == collection.owner.id) { - unawaited( - routeToPage( - context, - ShareCollectionPage(collection), - ), - ); - } else { - await showGenericErrorDialog(context: context, error: result); - _logger.severe("Cannot share collections owned by others"); - } - } - return result; - } - Future _showShareCollectionPage( BuildContext context, Collection collection, @@ -379,11 +341,11 @@ class AlbumVerticalListWidget extends StatelessWidget { context, collectionID, showProgressDialog, - selectedFiles: selectedFiles?.files.toList(), - sharedFiles: sharedFiles, + selectedFiles: widget.selectedFiles?.files.toList(), + sharedFiles: widget.sharedFiles, ); if (result) { - selectedFiles?.clearAll(); + widget.selectedFiles?.clearAll(); } return result; } @@ -393,8 +355,8 @@ class AlbumVerticalListWidget extends StatelessWidget { int toCollectionID, ) async { late final String message; - if (actionType == CollectionActionType.moveFiles || - actionType == CollectionActionType.moveToHiddenCollection) { + if (widget.actionType == CollectionActionType.moveFiles || + widget.actionType == CollectionActionType.moveToHiddenCollection) { message = S.of(context).movingFilesToAlbum; } else { message = S.of(context).unhidingFilesToAlbum; @@ -403,15 +365,16 @@ class AlbumVerticalListWidget extends StatelessWidget { final dialog = createProgressDialog(context, message, isDismissible: true); await dialog.show(); try { - final int fromCollectionID = selectedFiles!.files.first.collectionID!; + final int fromCollectionID = + widget.selectedFiles!.files.first.collectionID!; await CollectionsService.instance.move( - selectedFiles!.files.toList(), + widget.selectedFiles!.files.toList(), toCollectionID: toCollectionID, fromCollectionID: fromCollectionID, ); await dialog.hide(); unawaited(RemoteSyncService.instance.sync(silently: true)); - selectedFiles?.clearAll(); + widget.selectedFiles?.clearAll(); return true; } on AssertionError catch (e) { @@ -439,9 +402,9 @@ class AlbumVerticalListWidget extends StatelessWidget { await dialog.show(); try { await CollectionsService.instance - .restore(toCollectionID, selectedFiles!.files.toList()); + .restore(toCollectionID, widget.selectedFiles!.files.toList()); unawaited(RemoteSyncService.instance.sync(silently: true)); - selectedFiles?.clearAll(); + widget.selectedFiles?.clearAll(); await dialog.hide(); return true; } on AssertionError catch (e) { diff --git a/mobile/lib/ui/collections/collection_action_sheet.dart b/mobile/lib/ui/collections/collection_action_sheet.dart index 370982c8a6..b0441c21d3 100644 --- a/mobile/lib/ui/collections/collection_action_sheet.dart +++ b/mobile/lib/ui/collections/collection_action_sheet.dart @@ -11,6 +11,8 @@ import 'package:photos/models/selected_files.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/theme/colors.dart'; import 'package:photos/theme/ente_theme.dart'; +import "package:photos/ui/actions/collection/collection_file_actions.dart"; +import "package:photos/ui/actions/collection/collection_sharing_actions.dart"; import 'package:photos/ui/collections/album/vertical_list.dart'; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/components/bottom_of_title_bar_widget.dart'; @@ -18,6 +20,8 @@ import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import "package:photos/ui/components/text_input_widget.dart"; import 'package:photos/ui/components/title_bar_title_widget.dart'; +import "package:photos/ui/notification/toast.dart"; +import "package:photos/utils/separators_util.dart"; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; enum CollectionActionType { @@ -26,7 +30,6 @@ enum CollectionActionType { restoreFiles, unHide, shareCollection, - collectPhotos, addToHiddenAlbum, moveToHiddenCollection, } @@ -53,10 +56,9 @@ String _actionName( case CollectionActionType.shareCollection: text = S.of(context).share; break; - case CollectionActionType.collectPhotos: - text = S.of(context).share; case CollectionActionType.addToHiddenAlbum: text = S.of(context).addToHiddenAlbum; + break; case CollectionActionType.moveToHiddenCollection: text = S.of(context).moveToHiddenAlbum; break; @@ -90,7 +92,7 @@ void showCollectionActionSheet( topControl: const SizedBox.shrink(), backgroundColor: getEnteColorScheme(context).backgroundElevated, barrierColor: backdropFaintDark, - enableDrag: false, + enableDrag: true, ); } @@ -113,8 +115,10 @@ class CollectionActionSheet extends StatefulWidget { class _CollectionActionSheetState extends State { late final bool _showOnlyHiddenCollections; - static const int cancelButtonSize = 80; + late final bool _enableSelection; + static const int okButtonSize = 80; String _searchQuery = ""; + final _selectedCollections = []; @override void initState() { @@ -122,6 +126,9 @@ class _CollectionActionSheetState extends State { _showOnlyHiddenCollections = widget.actionType == CollectionActionType.moveToHiddenCollection || widget.actionType == CollectionActionType.addToHiddenAlbum; + _enableSelection = (widget.actionType == CollectionActionType.addFiles || + widget.actionType == CollectionActionType.addToHiddenAlbum) && + (widget.sharedFiles == null || widget.sharedFiles!.isEmpty); } @override @@ -129,11 +136,13 @@ class _CollectionActionSheetState extends State { final filesCount = widget.sharedFiles != null ? widget.sharedFiles!.length : widget.selectedFiles?.files.length ?? 0; - final bottomInset = MediaQuery.of(context).viewInsets.bottom; + final bottomInset = MediaQuery.viewInsetsOf(context).bottom; final isKeyboardUp = bottomInset > 100; + final double bottomPadding = + max(0, bottomInset - (_enableSelection ? okButtonSize : 0)); return Padding( padding: EdgeInsets.only( - bottom: isKeyboardUp ? bottomInset - cancelButtonSize : 0, + bottom: isKeyboardUp ? bottomPadding : 0, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -161,6 +170,7 @@ class _CollectionActionSheetState extends State { caption: widget.showOptionToCreateNewAlbum ? S.of(context).createOrSelectAlbum : S.of(context).selectAlbum, + showCloseButton: true, ), Padding( padding: const EdgeInsets.only( @@ -178,7 +188,6 @@ class _CollectionActionSheetState extends State { }, isClearable: true, shouldUnfocusOnClearOrSubmit: true, - borderRadius: 2, ), ), _getCollectionItems(), @@ -192,15 +201,16 @@ class _CollectionActionSheetState extends State { decoration: BoxDecoration( border: Border( top: BorderSide( - color: getEnteColorScheme(context).strokeFaint, + color: _enableSelection + ? getEnteColorScheme(context).strokeFaint + : Colors.transparent, ), ), ), - child: ButtonWidget( - buttonType: ButtonType.secondary, - buttonAction: ButtonAction.cancel, - isInAlert: true, - labelText: S.of(context).cancel, + child: Column( + children: [ + ..._actionButtons(), + ], ), ), ), @@ -213,6 +223,46 @@ class _CollectionActionSheetState extends State { ); } + List _actionButtons() { + final List widgets = []; + if (_enableSelection) { + widgets.add( + ButtonWidget( + key: const ValueKey('add_button'), + buttonType: ButtonType.primary, + isInAlert: true, + labelText: S.of(context).add, + shouldSurfaceExecutionStates: false, + isDisabled: _selectedCollections.isEmpty, + onTap: () async { + final CollectionActions collectionActions = + CollectionActions(CollectionsService.instance); + final result = await collectionActions.addToMultipleCollections( + context, + _selectedCollections, + true, + selectedFiles: widget.selectedFiles?.files.toList(), + ); + if (result) { + showShortToast( + context, + "Added successfully to " + + _selectedCollections.length.toString() + + " albums", + ); + widget.selectedFiles?.clearAll(); + } + }, + ), + ); + } + final widgetsWithSpaceBetween = addSeparators( + widgets, + const SizedBox(height: 8), + ); + return widgetsWithSpaceBetween; + } + Flexible _getCollectionItems() { return Flexible( child: Padding( @@ -239,6 +289,7 @@ class _CollectionActionSheetState extends State { : collections; return Scrollbar( thumbVisibility: true, + interactive: true, radius: const Radius.circular(2), child: Padding( padding: const EdgeInsets.only(right: 12), @@ -249,6 +300,11 @@ class _CollectionActionSheetState extends State { widget.sharedFiles, _searchQuery, shouldShowCreateAlbum, + enableSelection: _enableSelection, + selectedCollections: _selectedCollections, + onSelectionChanged: () { + setState(() {}); + }, ), ), ); @@ -273,14 +329,12 @@ class _CollectionActionSheetState extends State { }); return hiddenCollections; } else { - final bool includeUncategorized = - widget.actionType == CollectionActionType.restoreFiles; final List collections = CollectionsService.instance.getCollectionsForUI( // in collections where user is a collaborator, only addTo and remove // action can to be performed includeCollab: widget.actionType == CollectionActionType.addFiles, - includeUncategorized: includeUncategorized, + includeUncategorized: true, ); collections.sort((first, second) { return compareAsciiLowerCaseNatural( @@ -296,8 +350,7 @@ class _CollectionActionSheetState extends State { if (collection.isQuickLinkCollection() || collection.type == CollectionType.favorites || collection.type == CollectionType.uncategorized) { - if (collection.type == CollectionType.uncategorized && - includeUncategorized) { + if (collection.type == CollectionType.uncategorized) { uncategorized = collection; } continue; @@ -308,13 +361,12 @@ class _CollectionActionSheetState extends State { unpinned.add(collection); } } - return pinned + unpinned + (uncategorized != null ? [uncategorized] : []); + return (uncategorized != null ? [uncategorized] + pinned + unpinned : []); } } void _removeIncomingCollections(List items) { - if (widget.actionType == CollectionActionType.shareCollection || - widget.actionType == CollectionActionType.collectPhotos) { + if (widget.actionType == CollectionActionType.shareCollection) { final ownerID = Configuration.instance.getUserID(); items.removeWhere( (e) => !e.isOwner(ownerID!), diff --git a/mobile/lib/ui/collections/collection_list_page.dart b/mobile/lib/ui/collections/collection_list_page.dart index 974acdfdbb..3bc168a9d6 100644 --- a/mobile/lib/ui/collections/collection_list_page.dart +++ b/mobile/lib/ui/collections/collection_list_page.dart @@ -2,11 +2,20 @@ import "dart:async"; import 'package:flutter/material.dart'; import "package:photos/core/event_bus.dart"; +import "package:photos/events/album_sort_order_change_event.dart"; import "package:photos/events/collection_updated_event.dart"; +import "package:photos/generated/l10n.dart"; import 'package:photos/models/collection/collection.dart'; import 'package:photos/models/collection/collection_items.dart'; +import "package:photos/models/selected_albums.dart"; +import "package:photos/service_locator.dart"; import "package:photos/services/collections_service.dart"; +import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/collections/flex_grid_view.dart"; +import "package:photos/ui/components/buttons/icon_button_widget.dart"; +import "package:photos/ui/components/searchable_appbar.dart"; +import "package:photos/ui/viewer/actions/album_selection_overlay_bar.dart"; +import "package:photos/utils/local_settings.dart"; enum UISectionType { incomingCollections, @@ -38,6 +47,12 @@ class _CollectionListPageState extends State { late StreamSubscription _collectionUpdatesSubscription; List? collections; + AlbumSortKey? sortKey; + AlbumViewType? albumViewType; + AlbumSortDirection? albumSortDirection; + String _searchQuery = ""; + final _selectedAlbum = SelectedAlbums(); + late final ScrollController _scrollController; @override void initState() { @@ -47,36 +62,77 @@ class _CollectionListPageState extends State { Bus.instance.on().listen((event) async { unawaited(refreshCollections()); }); + sortKey = localSettings.albumSortKey(); + albumViewType = localSettings.albumViewType(); + albumSortDirection = localSettings.albumSortDirection(); + _scrollController = ScrollController( + initialScrollOffset: widget.initialScrollOffset ?? 0, + ); } @override void dispose() { _collectionUpdatesSubscription.cancel(); + _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { + final displayLimitCount = (collections?.length ?? 0) + + (widget.tag.isEmpty && _searchQuery.isEmpty ? 1 : 0); + final bool enableSelectionMode = + widget.sectionType == UISectionType.homeCollections || + widget.sectionType == UISectionType.outgoingCollections || + widget.sectionType == UISectionType.incomingCollections; return Scaffold( body: SafeArea( - child: CustomScrollView( - physics: const BouncingScrollPhysics(), - controller: ScrollController( - initialScrollOffset: widget.initialScrollOffset ?? 0, - ), - slivers: [ - SliverAppBar( - elevation: 0, - title: Hero( - tag: widget.tag, - child: widget.appTitle ?? const SizedBox.shrink(), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Scrollbar( + interactive: true, + controller: _scrollController, + child: CustomScrollView( + physics: const BouncingScrollPhysics(), + controller: _scrollController, + slivers: [ + SearchableAppBar( + title: widget.appTitle ?? const SizedBox.shrink(), + heroTag: widget.tag, + onSearch: (value) { + setState(() { + _searchQuery = value; + }); + refreshCollections(); + }, + onSearchClosed: () { + setState(() { + _searchQuery = ''; + }); + refreshCollections(); + }, + actions: [ + _sortMenu(collections!), + ], + ), + CollectionsFlexiGridViewWidget( + collections, + displayLimitCount: displayLimitCount, + tag: widget.tag, + enableSelectionMode: enableSelectionMode, + albumViewType: albumViewType ?? AlbumViewType.grid, + selectedAlbums: _selectedAlbum, + scrollBottomSafeArea: 140, + ), + ], ), - floating: true, ), - CollectionsFlexiGridViewWidget( - collections, - displayLimitCount: collections?.length ?? 0, - tag: widget.tag, + AlbumSelectionOverlayBar( + _selectedAlbum, + widget.sectionType, + collections!, + showSelectAllButton: true, ), ], ), @@ -84,19 +140,133 @@ class _CollectionListPageState extends State { ); } + Widget _sortMenu(List collections) { + final colorTheme = getEnteColorScheme(context); + + Widget sortOptionText(AlbumSortKey key) { + String text = key.toString(); + switch (key) { + case AlbumSortKey.albumName: + text = S.of(context).name; + break; + case AlbumSortKey.newestPhoto: + text = S.of(context).newest; + break; + case AlbumSortKey.lastUpdated: + text = S.of(context).lastUpdated; + } + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + text, + ), + Icon( + sortKey == key + ? (albumSortDirection == AlbumSortDirection.ascending + ? Icons.arrow_upward + : Icons.arrow_downward) + : null, + size: 18, + ), + ], + ); + } + + return Theme( + data: Theme.of(context).copyWith( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + ), + child: Row( + children: [ + IconButtonWidget( + icon: albumViewType == AlbumViewType.grid + ? Icons.view_list_outlined + : Icons.grid_view_outlined, + iconButtonType: IconButtonType.secondary, + iconColor: colorTheme.blurStrokePressed, + onTap: () async { + setState(() { + albumViewType = albumViewType == AlbumViewType.grid + ? AlbumViewType.list + : AlbumViewType.grid; + }); + await localSettings.setAlbumViewType(albumViewType!); + }, + ), + GestureDetector( + onTapDown: (TapDownDetails details) async { + final int? selectedValue = await showMenu( + color: colorTheme.backgroundElevated, + context: context, + position: RelativeRect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + details.globalPosition.dx, + details.globalPosition.dy + 50, + ), + items: List.generate(AlbumSortKey.values.length, (index) { + return PopupMenuItem( + value: index, + child: sortOptionText(AlbumSortKey.values[index]), + ); + }), + ); + if (selectedValue != null) { + sortKey = AlbumSortKey.values[selectedValue]; + await localSettings.setAlbumSortKey(sortKey!); + albumSortDirection = + albumSortDirection == AlbumSortDirection.ascending + ? AlbumSortDirection.descending + : AlbumSortDirection.ascending; + await localSettings.setAlbumSortDirection(albumSortDirection!); + await refreshCollections(); + Bus.instance.fire(AlbumSortOrderChangeEvent()); + } + }, + child: IconButtonWidget( + icon: Icons.sort_outlined, + iconButtonType: IconButtonType.secondary, + iconColor: colorTheme.blurStrokePressed, + ), + ), + ], + ), + ); + } + Future refreshCollections() async { if (widget.sectionType == UISectionType.incomingCollections || widget.sectionType == UISectionType.outgoingCollections) { final SharedCollections sharedCollections = - CollectionsService.instance.getSharedCollections(); + await CollectionsService.instance.getSharedCollections(); if (widget.sectionType == UISectionType.incomingCollections) { collections = sharedCollections.incoming; } else { collections = sharedCollections.outgoing; } + if (_searchQuery.isNotEmpty) { + collections = widget.collections + ?.where( + (c) => c.displayName + .toLowerCase() + .contains(_searchQuery.toLowerCase()), + ) + .toList(); + } } else if (widget.sectionType == UISectionType.homeCollections) { collections = await CollectionsService.instance.getCollectionForOnEnteSection(); + if (_searchQuery.isNotEmpty) { + collections = widget.collections + ?.where( + (c) => c.displayName + .toLowerCase() + .contains(_searchQuery.toLowerCase()), + ) + .toList(); + } } if (mounted) { setState(() {}); diff --git a/mobile/lib/ui/collections/device/device_folders_vertical_grid_view.dart b/mobile/lib/ui/collections/device/device_folders_vertical_grid_view.dart index d42bad8dd5..0e5a19edf6 100644 --- a/mobile/lib/ui/collections/device/device_folders_vertical_grid_view.dart +++ b/mobile/lib/ui/collections/device/device_folders_vertical_grid_view.dart @@ -66,8 +66,8 @@ class _DeviceFolderVerticalGridViewBodyState Width changes dynamically with screen width such that we can fit 2 in one row. Keep the width integral (center the albums to distribute excess pixels) */ - static const maxThumbnailWidth = 224.0; - static const fixedGapBetweenAlbum = 8.0; + static const maxThumbnailWidth = 170.0; + static const fixedGapBetweenAlbum = 2.0; static const minGapForHorizontalPadding = 8.0; @override @@ -103,7 +103,7 @@ class _DeviceFolderVerticalGridViewBodyState if (snapshot.hasData) { final double screenWidth = MediaQuery.of(context).size.width; final int albumsCountInOneRow = - max(screenWidth ~/ maxThumbnailWidth, 2); + max(screenWidth ~/ maxThumbnailWidth, 3); final double gapBetweenAlbums = (albumsCountInOneRow - 1) * fixedGapBetweenAlbum; final double gapOnSizeOfAlbums = minGapForHorizontalPadding + diff --git a/mobile/lib/ui/collections/flex_grid_view.dart b/mobile/lib/ui/collections/flex_grid_view.dart index 4114ff0493..5080167937 100644 --- a/mobile/lib/ui/collections/flex_grid_view.dart +++ b/mobile/lib/ui/collections/flex_grid_view.dart @@ -1,20 +1,34 @@ +import "dart:async"; import 'dart:math'; import 'package:flutter/material.dart'; +import "package:flutter/services.dart"; +import "package:logging/logging.dart"; +import "package:photos/core/event_bus.dart"; +import "package:photos/events/clear_album_selections_event.dart"; +import "package:photos/generated/l10n.dart"; import 'package:photos/models/collection/collection.dart'; +import "package:photos/models/collection/collection_items.dart"; +import "package:photos/models/selected_albums.dart"; +import "package:photos/services/collections_service.dart"; +import "package:photos/ui/collections/album/list_item.dart"; +import "package:photos/ui/collections/album/new_list_item.dart"; +import "package:photos/ui/collections/album/new_row_item.dart"; import "package:photos/ui/collections/album/row_item.dart"; +import "package:photos/ui/viewer/gallery/collection_page.dart"; +import "package:photos/utils/dialog_util.dart"; +import "package:photos/utils/local_settings.dart"; +import "package:photos/utils/navigation_util.dart"; -class CollectionsFlexiGridViewWidget extends StatelessWidget { +class CollectionsFlexiGridViewWidget extends StatefulWidget { /* - Aspect ratio 1:1 Max width 224 Fixed gap 8 - Width changes dynamically with screen width such that we can fit 2 in one row. - Keep the width integral (center the albums to distribute excess pixels) - */ - static const maxThumbnailWidth = 224.0; - static const fixedGapBetweenAlbum = 8.0; - static const minGapForHorizontalPadding = 8.0; - static const collectionItemsToPreload = 20; + Aspect ratio 1:1 + Width changes dynamically with screen width + */ + static const maxThumbnailWidth = 224.0; + static const crossAxisSpacing = 8.0; + static const horizontalPadding = 16.0; final List? collections; // At max how many albums to display final int displayLimitCount; @@ -23,49 +37,257 @@ class CollectionsFlexiGridViewWidget extends StatelessWidget { final bool shrinkWrap; final String tag; + final AlbumViewType albumViewType; + final bool enableSelectionMode; + final bool shouldShowCreateAlbum; + final SelectedAlbums? selectedAlbums; + final double scrollBottomSafeArea; + final bool onlyAllowSelection; + const CollectionsFlexiGridViewWidget( this.collections, { - this.displayLimitCount = 10, + this.displayLimitCount = 9, this.shrinkWrap = false, this.tag = "", + this.enableSelectionMode = false, super.key, + this.albumViewType = AlbumViewType.grid, + this.shouldShowCreateAlbum = false, + this.selectedAlbums, + this.scrollBottomSafeArea = 8, + this.onlyAllowSelection = false, }); @override - Widget build(BuildContext context) { - final double screenWidth = MediaQuery.of(context).size.width; - final int albumsCountInOneRow = max(screenWidth ~/ maxThumbnailWidth, 2); - final double gapBetweenAlbums = - (albumsCountInOneRow - 1) * fixedGapBetweenAlbum; - // gapOnSizeOfAlbums will be - final double gapOnSizeOfAlbums = minGapForHorizontalPadding + - (screenWidth - gapBetweenAlbums - (2 * minGapForHorizontalPadding)) % - albumsCountInOneRow; + State createState() => + _CollectionsFlexiGridViewWidgetState(); +} - final double sideOfThumbnail = - (screenWidth - gapOnSizeOfAlbums - gapBetweenAlbums) / - albumsCountInOneRow; +class _CollectionsFlexiGridViewWidgetState + extends State { + bool isAnyAlbumSelected = false; + late StreamSubscription + _clearAlbumSelectionSubscription; + + @override + void initState() { + _clearAlbumSelectionSubscription = + Bus.instance.on().listen((event) { + if (mounted) { + setState(() { + isAnyAlbumSelected = false; + }); + } + }); + super.initState(); + } + + @override + void dispose() { + _clearAlbumSelectionSubscription.cancel(); + super.dispose(); + } + + Future _toggleAlbumSelection(Collection c) async { + await HapticFeedback.lightImpact(); + widget.selectedAlbums!.toggleSelection(c); + setState(() { + isAnyAlbumSelected = widget.selectedAlbums!.albums.isNotEmpty; + }); + } + + Future _navigateToCollectionPage(Collection c) async { + final thumbnail = await CollectionsService.instance.getCover(c); + // ignore: unawaited_futures + routeToPage( + context, + CollectionPage( + CollectionWithThumbnail(c, thumbnail), + ), + ); + } + + @override + Widget build(BuildContext context) { + return widget.albumViewType == AlbumViewType.grid + ? _buildGridView(context, const ValueKey("grid_view")) + : _buildListView(context, const ValueKey("list_view")); + } + + Widget _buildGridView(BuildContext context, Key key) { + final double screenWidth = MediaQuery.sizeOf(context).width; + final int albumsCountInCrossAxis = + max(screenWidth ~/ CollectionsFlexiGridViewWidget.maxThumbnailWidth, 3); + final double totalCrossAxisSpacing = (albumsCountInCrossAxis - 1) * + CollectionsFlexiGridViewWidget.crossAxisSpacing; + + final double sideOfThumbnail = (screenWidth - + totalCrossAxisSpacing - + CollectionsFlexiGridViewWidget.horizontalPadding) / + albumsCountInCrossAxis; + + final int totalCollections = widget.collections!.length; + final bool showCreateAlbum = widget.shouldShowCreateAlbum; + final int totalItemCount = totalCollections + (showCreateAlbum ? 1 : 0); + final int displayItemCount = min(totalItemCount, widget.displayLimitCount); return SliverPadding( - padding: const EdgeInsets.all(8), + key: key, + padding: EdgeInsets.only( + top: 8, + left: CollectionsFlexiGridViewWidget.horizontalPadding / 2, + right: CollectionsFlexiGridViewWidget.horizontalPadding / 2, + bottom: widget.scrollBottomSafeArea, + ), sliver: SliverGrid( delegate: SliverChildBuilderDelegate( (context, index) { + if (showCreateAlbum && index == 0) { + return NewAlbumRowItemWidget( + height: sideOfThumbnail, + width: sideOfThumbnail, + ); + } + final collectionIndex = showCreateAlbum ? index - 1 : index; return AlbumRowItemWidget( - collections![index], + widget.collections![collectionIndex], sideOfThumbnail, - tag: tag, + tag: widget.tag, + selectedAlbums: widget.selectedAlbums, + onTapCallback: (c) { + isAnyAlbumSelected || widget.onlyAllowSelection + ? _toggleAlbumSelection(c) + : _navigateToCollectionPage(c); + }, + onLongPressCallback: widget.enableSelectionMode + ? (c) { + isAnyAlbumSelected || widget.onlyAllowSelection + ? _navigateToCollectionPage(c) + : _toggleAlbumSelection(c); + } + : null, ); }, - childCount: min(collections!.length, displayLimitCount), + childCount: displayItemCount, ), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: albumsCountInOneRow, - mainAxisSpacing: 4, - crossAxisSpacing: gapBetweenAlbums, + crossAxisCount: albumsCountInCrossAxis, + mainAxisSpacing: 2, + crossAxisSpacing: CollectionsFlexiGridViewWidget.crossAxisSpacing, childAspectRatio: sideOfThumbnail / (sideOfThumbnail + 46), ), ), ); } + + Widget _buildListView(BuildContext context, Key key) { + final int totalCollections = widget.collections?.length ?? 0; + final bool showCreateAlbum = + widget.shouldShowCreateAlbum && !isAnyAlbumSelected; + final int totalItemCount = totalCollections + (showCreateAlbum ? 1 : 0); + final int displayItemCount = min(totalItemCount, widget.displayLimitCount); + + return SliverPadding( + key: key, + padding: EdgeInsets.only( + top: 8, + left: 8, + right: 8, + bottom: widget.scrollBottomSafeArea, + ), + sliver: SliverPrototypeExtentList( + prototypeItem: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: showCreateAlbum + ? const NewAlbumListItemWidget() + : AlbumListItemWidget( + widget.collections![0], + selectedAlbums: widget.selectedAlbums, + onTapCallback: (c) {}, + ), + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + Widget item; + + if (showCreateAlbum && index == 0) { + item = GestureDetector( + onTap: () async { + GestureDetector( + onTap: () async { + final result = await showTextInputDialog( + context, + title: S.of(context).newAlbum, + submitButtonLabel: S.of(context).create, + hintText: S.of(context).enterAlbumName, + alwaysShowSuccessState: false, + initialValue: "", + textCapitalization: TextCapitalization.words, + popnavAfterSubmission: false, + onSubmit: (String text) async { + if (text.trim() == "") { + return; + } + + try { + final Collection c = await CollectionsService + .instance + .createAlbum(text); + // ignore: unawaited_futures + await routeToPage( + context, + CollectionPage(CollectionWithThumbnail(c, null)), + ); + Navigator.of(context).pop(); + } catch (e, s) { + Logger("CreateNewAlbumIcon") + .severe("Failed to rename album", e, s); + rethrow; + } + }, + ); + + if (result is Exception) { + await showGenericErrorDialog( + context: context, + error: result, + ); + } + }, + child: const NewAlbumListItemWidget(), + ); + }, + child: const NewAlbumListItemWidget(), + ); + } else { + final collectionIndex = showCreateAlbum ? index - 1 : index; + + item = AlbumListItemWidget( + widget.collections![collectionIndex], + selectedAlbums: widget.selectedAlbums, + onTapCallback: (c) { + isAnyAlbumSelected + ? _toggleAlbumSelection(c) + : _navigateToCollectionPage(c); + }, + onLongPressCallback: widget.enableSelectionMode + ? (c) { + isAnyAlbumSelected + ? _navigateToCollectionPage(c) + : _toggleAlbumSelection(c); + } + : null, + ); + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: item, + ); + }, + childCount: displayItemCount, + ), + ), + ); + } } diff --git a/mobile/lib/ui/common/date_input.dart b/mobile/lib/ui/common/date_input.dart index 41fbccb76a..f9624d5167 100644 --- a/mobile/lib/ui/common/date_input.dart +++ b/mobile/lib/ui/common/date_input.dart @@ -121,8 +121,10 @@ class _DatePickerFieldState extends State { } Future _showDatePicker() async { + final Locale locale = await getFormatLocale(); final DateTime? picked = await showDatePicker( context: context, + locale: locale, initialDate: _selectedDate ?? DateTime.now(), firstDate: widget.firstDate ?? DateTime(1900), lastDate: widget.lastDate ?? DateTime(2100), diff --git a/mobile/lib/ui/common/loading_widget.dart b/mobile/lib/ui/common/loading_widget.dart index ffdc438901..fc10eeb18a 100644 --- a/mobile/lib/ui/common/loading_widget.dart +++ b/mobile/lib/ui/common/loading_widget.dart @@ -22,10 +22,12 @@ class EnteLoadingWidget extends StatelessWidget { padding: EdgeInsets.all(padding), child: SizedBox.fromSize( size: Size.square(size), - child: CircularProgressIndicator( - strokeWidth: 2, - color: color ?? getEnteColorScheme(context).strokeBase, - strokeCap: StrokeCap.round, + child: RepaintBoundary( + child: CircularProgressIndicator( + strokeWidth: 2, + color: color ?? getEnteColorScheme(context).strokeBase, + strokeCap: StrokeCap.round, + ), ), ), ), diff --git a/mobile/lib/ui/components/bottom_action_bar/album_action_bar_widget.dart b/mobile/lib/ui/components/bottom_action_bar/album_action_bar_widget.dart new file mode 100644 index 0000000000..db4dee8d9a --- /dev/null +++ b/mobile/lib/ui/components/bottom_action_bar/album_action_bar_widget.dart @@ -0,0 +1,86 @@ +import "package:flutter/material.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/models/selected_albums.dart"; +import "package:photos/theme/ente_theme.dart"; + +class AlbumActionBarWidget extends StatefulWidget { + final SelectedAlbums? selectedAlbums; + final VoidCallback? onCancel; + const AlbumActionBarWidget({ + super.key, + this.selectedAlbums, + this.onCancel, + }); + + @override + State createState() => _AlbumActionBarWidgetState(); +} + +class _AlbumActionBarWidgetState extends State { + final ValueNotifier _selectedAlbumNotifier = ValueNotifier(0); + + @override + void initState() { + widget.selectedAlbums?.addListener(_selectedAlbumListener); + super.initState(); + } + + @override + void dispose() { + widget.selectedAlbums?.removeListener(_selectedAlbumListener); + _selectedAlbumNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final textTheme = getEnteTextTheme(context); + return SizedBox( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + flex: 1, + child: ValueListenableBuilder( + valueListenable: _selectedAlbumNotifier, + builder: (context, value, child) { + return Text( + S.of(context).selectedAlbums( + widget.selectedAlbums?.albums.length ?? 0, + ), + style: textTheme.miniMuted, + ); + }, + ), + ), + Flexible( + flex: 1, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + widget.onCancel?.call(); + }, + child: Align( + alignment: Alignment.centerRight, + child: Text( + S.of(context).cancel, + style: textTheme.mini, + ), + ), + ), + ), + ), + ], + ), + ), + ); + } + + void _selectedAlbumListener() { + _selectedAlbumNotifier.value = widget.selectedAlbums?.albums.length ?? 0; + } +} diff --git a/mobile/lib/ui/components/bottom_action_bar/album_bottom_action_bar_widget.dart b/mobile/lib/ui/components/bottom_action_bar/album_bottom_action_bar_widget.dart new file mode 100644 index 0000000000..3312584085 --- /dev/null +++ b/mobile/lib/ui/components/bottom_action_bar/album_bottom_action_bar_widget.dart @@ -0,0 +1,59 @@ +import "package:flutter/material.dart"; +import "package:photos/core/constants.dart"; +import "package:photos/models/selected_albums.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/collections/collection_list_page.dart"; +import "package:photos/ui/components/bottom_action_bar/album_action_bar_widget.dart"; +import "package:photos/ui/components/divider_widget.dart"; +import "package:photos/ui/viewer/actions/album_selection_action_widget.dart"; + +class AlbumBottomActionBarWidget extends StatelessWidget { + final SelectedAlbums selectedAlbums; + final VoidCallback? onCancel; + final Color? backgroundColor; + final UISectionType sectionType; + + const AlbumBottomActionBarWidget( + this.selectedAlbums, + this.sectionType, { + super.key, + this.backgroundColor, + this.onCancel, + }); + + @override + Widget build(BuildContext context) { + final bottomPadding = MediaQuery.paddingOf(context).bottom; + final widthOfScreen = MediaQuery.sizeOf(context).width; + final colorScheme = getEnteColorScheme(context); + final double leftRightPadding = widthOfScreen > restrictedMaxWidth + ? (widthOfScreen - restrictedMaxWidth) / 2 + : 0; + return Container( + decoration: BoxDecoration( + color: backgroundColor ?? colorScheme.backgroundElevated2, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + topRight: Radius.circular(8), + ), + ), + padding: EdgeInsets.only( + top: 4, + bottom: bottomPadding, + right: leftRightPadding, + left: leftRightPadding, + ), + child: Column( + children: [ + const SizedBox(height: 8), + AlbumSelectionActionWidget(selectedAlbums, sectionType), + const DividerWidget(dividerType: DividerType.bottomBar), + AlbumActionBarWidget( + selectedAlbums: selectedAlbums, + onCancel: onCancel, + ), + ], + ), + ); + } +} diff --git a/mobile/lib/ui/components/searchable_appbar.dart b/mobile/lib/ui/components/searchable_appbar.dart new file mode 100644 index 0000000000..4290713552 --- /dev/null +++ b/mobile/lib/ui/components/searchable_appbar.dart @@ -0,0 +1,132 @@ +import "package:flutter/material.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/components/buttons/icon_button_widget.dart"; + +class SearchableAppBar extends StatefulWidget { + final Widget title; + final List? actions; + final Function(String) onSearch; + final VoidCallback? onSearchClosed; + final String heroTag; + + const SearchableAppBar({ + super.key, + required this.title, + this.actions, + required this.onSearch, + this.onSearchClosed, + this.heroTag = "", + }); + + @override + State createState() => _SearchableAppBarState(); +} + +class _SearchableAppBarState extends State { + bool _isSearchActive = false; + final TextEditingController _searchController = TextEditingController(); + final FocusNode _searchFocusNode = FocusNode(); + + @override + void dispose() { + _searchController.dispose(); + _searchFocusNode.dispose(); + super.dispose(); + } + + void _activateSearch() { + setState(() { + _isSearchActive = true; + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + _searchFocusNode.requestFocus(); + }); + } + + void _deactivateSearch() { + setState(() { + _isSearchActive = false; + _searchController.clear(); + }); + _searchFocusNode.unfocus(); + if (widget.onSearchClosed != null) { + widget.onSearchClosed!(); + } + } + + @override + Widget build(BuildContext context) { + return SliverAppBar( + floating: true, + elevation: 0, + automaticallyImplyLeading: !_isSearchActive, + title: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + child: _isSearchActive + ? _buildSearchField() + : Hero( + key: const ValueKey('titleBar'), + tag: widget.heroTag, + child: widget.title, + ), + ), + actions: _isSearchActive + ? null + : [ + IconButtonWidget( + icon: Icons.search, + iconButtonType: IconButtonType.secondary, + onTap: _activateSearch, + iconColor: getEnteColorScheme(context).blurStrokePressed, + ), + ...?widget.actions, + ], + ); + } + + Widget _buildSearchField() { + final colorScheme = getEnteColorScheme(context); + return Container( + key: const ValueKey('searchBar'), + alignment: Alignment.center, + child: TextFormField( + controller: _searchController, + focusNode: _searchFocusNode, + decoration: InputDecoration( + filled: true, + fillColor: colorScheme.fillFaint, + prefixIcon: Icon( + Icons.search_rounded, + color: colorScheme.strokeMuted, + ), + suffixIcon: IconButton( + icon: Icon( + Icons.cancel_rounded, + color: colorScheme.strokeMuted, + ), + onPressed: _deactivateSearch, + ), + border: const UnderlineInputBorder( + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.fromLTRB(12, 12, 0, 12), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: colorScheme.strokeFaint, + ), + borderRadius: BorderRadius.circular(8), + ), + ), + onChanged: widget.onSearch, + ), + ); + } +} diff --git a/mobile/lib/ui/components/title_bar_widget.dart b/mobile/lib/ui/components/title_bar_widget.dart index 9bdaf81c0f..2105d9b36b 100644 --- a/mobile/lib/ui/components/title_bar_widget.dart +++ b/mobile/lib/ui/components/title_bar_widget.dart @@ -14,6 +14,8 @@ class TitleBarWidget extends StatelessWidget { final bool isOnTopOfScreen; final Color? backgroundColor; final bool isSliver; + final double? expandedHeight; + const TitleBarWidget({ this.leading, this.title, @@ -26,6 +28,7 @@ class TitleBarWidget extends StatelessWidget { this.isOnTopOfScreen = true, this.backgroundColor, this.isSliver = true, + this.expandedHeight, super.key, }); @@ -40,7 +43,8 @@ class TitleBarWidget extends StatelessWidget { leadingWidth: 48, automaticallyImplyLeading: false, pinned: true, - expandedHeight: isFlexibleSpaceDisabled ? toolbarHeight : 102, + expandedHeight: + expandedHeight ?? (isFlexibleSpaceDisabled ? toolbarHeight : 102), centerTitle: false, titleSpacing: 4, title: TitleWidget( @@ -72,6 +76,7 @@ class TitleBarWidget extends StatelessWidget { flexibleSpaceTitle, flexibleSpaceCaption, toolbarHeight, + maxLines: expandedHeight == null ? 1 : 2, ), ); } else { @@ -112,6 +117,7 @@ class TitleBarWidget extends StatelessWidget { flexibleSpaceTitle, flexibleSpaceCaption, toolbarHeight, + maxLines: expandedHeight == null ? 1 : 2, ), ); } @@ -189,11 +195,14 @@ class FlexibleSpaceBarWidget extends StatelessWidget { final Widget? flexibleSpaceTitle; final String? flexibleSpaceCaption; final double toolbarHeight; + final int maxLines; + const FlexibleSpaceBarWidget( this.flexibleSpaceTitle, this.flexibleSpaceCaption, this.toolbarHeight, { super.key, + required this.maxLines, }); @override @@ -223,7 +232,7 @@ class FlexibleSpaceBarWidget extends StatelessWidget { flexibleSpaceCaption!, style: textTheme.smallMuted, overflow: TextOverflow.ellipsis, - maxLines: 1, + maxLines: maxLines, ), ], ), diff --git a/mobile/lib/ui/home/home_bottom_nav_bar.dart b/mobile/lib/ui/home/home_bottom_nav_bar.dart index 67a0d98cf9..382c3e8210 100644 --- a/mobile/lib/ui/home/home_bottom_nav_bar.dart +++ b/mobile/lib/ui/home/home_bottom_nav_bar.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/events/tab_changed_event.dart'; +import "package:photos/models/selected_albums.dart"; import 'package:photos/models/selected_files.dart'; import "package:photos/theme/colors.dart"; import 'package:photos/theme/ente_theme.dart'; @@ -10,12 +11,14 @@ import 'package:photos/ui/tabs/nav_bar.dart'; class HomeBottomNavigationBar extends StatefulWidget { const HomeBottomNavigationBar( - this.selectedFiles, { + this.selectedFiles, + this.selectedAlbums, { required this.selectedTabIndex, super.key, }); final SelectedFiles selectedFiles; + final SelectedAlbums selectedAlbums; final int selectedTabIndex; @override @@ -32,6 +35,7 @@ class _HomeBottomNavigationBarState extends State { super.initState(); currentTabIndex = widget.selectedTabIndex; widget.selectedFiles.addListener(_selectedFilesListener); + widget.selectedAlbums.addListener(_selectedAlbumsListener); _tabChangedEventSubscription = Bus.instance.on().listen((event) { if (event.source != TabChangedEventSource.tabBar) { @@ -56,6 +60,7 @@ class _HomeBottomNavigationBarState extends State { void dispose() { _tabChangedEventSubscription.cancel(); widget.selectedFiles.removeListener(_selectedFilesListener); + widget.selectedAlbums.removeListener(_selectedAlbumsListener); super.dispose(); } @@ -65,6 +70,12 @@ class _HomeBottomNavigationBarState extends State { } } + void _selectedAlbumsListener() { + if (mounted) { + setState(() {}); + } + } + void _onTabChange(int index, {String mode = 'tabChanged'}) { debugPrint("_TabChanged called via method $mode"); Bus.instance.fire( @@ -78,6 +89,7 @@ class _HomeBottomNavigationBarState extends State { @override Widget build(BuildContext context) { final bool filesAreSelected = widget.selectedFiles.files.isNotEmpty; + final bool albumsAreSelected = widget.selectedAlbums.albums.isNotEmpty; final enteColorScheme = getEnteColorScheme(context); return SafeArea( @@ -85,9 +97,9 @@ class _HomeBottomNavigationBarState extends State { child: AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, - height: filesAreSelected ? 0 : 56, + height: filesAreSelected || albumsAreSelected ? 0 : 56, child: IgnorePointer( - ignoring: filesAreSelected, + ignoring: filesAreSelected || albumsAreSelected, child: ListView( physics: const NeverScrollableScrollPhysics(), children: [ diff --git a/mobile/lib/ui/home/home_gallery_widget.dart b/mobile/lib/ui/home/home_gallery_widget.dart index 35bc5eb326..0ece05ba72 100644 --- a/mobile/lib/ui/home/home_gallery_widget.dart +++ b/mobile/lib/ui/home/home_gallery_widget.dart @@ -88,6 +88,7 @@ class _HomeGalleryWidgetState extends State { dedupeUploadID: true, ignoredCollectionIDs: collectionsToHide, ignoreSavedFiles: true, + ignoreSharedItems: _shouldHideSharedItems, ); if (hasSelectedAllForBackup) { result = await FilesDB.instance.getAllLocalAndUploadedFiles( @@ -97,7 +98,6 @@ class _HomeGalleryWidgetState extends State { limit: limit, asc: asc, filterOptions: filterOptions, - ignoreSharedFiles: _shouldHideSharedItems, ); } else { result = await FilesDB.instance.getAllPendingOrUploadedFiles( @@ -107,7 +107,6 @@ class _HomeGalleryWidgetState extends State { limit: limit, asc: asc, filterOptions: filterOptions, - ignoreSharedFiles: _shouldHideSharedItems, ); } diff --git a/mobile/lib/ui/home/landing_page_widget.dart b/mobile/lib/ui/home/landing_page_widget.dart index dda991b344..bdc1e8a03e 100644 --- a/mobile/lib/ui/home/landing_page_widget.dart +++ b/mobile/lib/ui/home/landing_page_widget.dart @@ -42,10 +42,6 @@ class _LandingPageWidgetState extends State { void initState() { super.initState(); Future(_showAutoLogoutDialogIfRequired); - Future.delayed( - const Duration(seconds: 1), - _autoEnableResumableUpload, - ); } @override @@ -308,10 +304,6 @@ class _LandingPageWidgetState extends State { } } } - - void _autoEnableResumableUpload() { - localSettings.autoEnableMultiplePart(20).ignore(); - } } class FeatureItemWidget extends StatelessWidget { diff --git a/mobile/lib/ui/home/memories/full_screen_memory.dart b/mobile/lib/ui/home/memories/full_screen_memory.dart index 7a09c80f2a..2313a0f366 100644 --- a/mobile/lib/ui/home/memories/full_screen_memory.dart +++ b/mobile/lib/ui/home/memories/full_screen_memory.dart @@ -1,20 +1,25 @@ import "dart:async"; import "dart:io"; +import "dart:math"; +import "dart:ui"; import "package:flutter/cupertino.dart"; import "package:flutter/material.dart"; import "package:photos/core/configuration.dart"; +import "package:photos/models/file/file_type.dart"; import "package:photos/models/memories/memory.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/smart_memories_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/theme/text_style.dart"; import "package:photos/ui/actions/file/file_actions.dart"; +import "package:photos/ui/home/memories/memory_progress_indicator.dart"; import "package:photos/ui/viewer/file/file_widget.dart"; +import "package:photos/ui/viewer/file/thumbnail_widget.dart"; import "package:photos/ui/viewer/file_details/favorite_widget.dart"; import "package:photos/utils/file_util.dart"; import "package:photos/utils/share_util.dart"; -import "package:step_progress_indicator/step_progress_indicator.dart"; +// import "package:step_progress_indicator/step_progress_indicator.dart"; //There are two states of variables that FullScreenMemory depends on: //1. The list of memories @@ -131,6 +136,10 @@ class FullScreenMemory extends StatefulWidget { class _FullScreenMemoryState extends State { PageController? _pageController; final _showTitle = ValueNotifier(true); + AnimationController? _progressAnimationController; + AnimationController? _zoomAnimationController; + final ValueNotifier durationNotifier = + ValueNotifier(const Duration(seconds: 5)); @override void initState() { @@ -148,13 +157,48 @@ class _FullScreenMemoryState extends State { void dispose() { _pageController?.dispose(); _showTitle.dispose(); + durationNotifier.dispose(); super.dispose(); } + void _toggleAnimation(bool pause) { + if (_progressAnimationController != null) { + if (pause) { + _progressAnimationController!.stop(); + } else { + _progressAnimationController!.forward(); + } + } + if (_zoomAnimationController != null) { + if (pause) { + _zoomAnimationController!.stop(); + } else { + _zoomAnimationController!.forward(); + } + } + } + + void onFinalFileLoad(int duration) { + if (_progressAnimationController!.isAnimating == true) { + _progressAnimationController!.stop(); + } + durationNotifier.value = Duration(seconds: duration); + _progressAnimationController + ?..stop() + ..reset() + ..duration = durationNotifier.value + ..forward(); + _zoomAnimationController + ?..stop() + ..reset() + ..forward(); + } + @override Widget build(BuildContext context) { final inheritedData = FullScreenMemoryData.of(context)!; final showStepProgressIndicator = inheritedData.memories.length < 60; + return Scaffold( backgroundColor: Colors.black, extendBodyBehindAppBar: true, @@ -180,17 +224,34 @@ class _FullScreenMemoryState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ showStepProgressIndicator - ? StepProgressIndicator( - totalSteps: inheritedData.memories.length, - currentStep: value + 1, - size: 2, - selectedColor: Colors.white, //same for both themes - unselectedColor: Colors.white.withOpacity(0.4), + ? ValueListenableBuilder( + valueListenable: durationNotifier, + builder: (context, duration, _) { + return NewProgressIndicator( + totalSteps: inheritedData.memories.length, + currentIndex: value, + selectedColor: Colors.white, + unselectedColor: Colors.white.withOpacity(0.4), + duration: duration, + animationController: (controller) { + _progressAnimationController = controller; + }, + onComplete: () { + final currentIndex = + inheritedData.indexNotifier.value; + if (currentIndex < + inheritedData.memories.length - 1) { + _pageController!.nextPage( + duration: const Duration(milliseconds: 250), + curve: Curves.ease, + ); + } + }, + ); + }, ) : const SizedBox.shrink(), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), Row( children: [ child!, @@ -231,16 +292,20 @@ class _FullScreenMemoryState extends State { body: Stack( alignment: Alignment.bottomCenter, children: [ + const MemoryBackDrop(), PageView.builder( controller: _pageController ??= PageController( initialPage: widget.initialIndex, ), + physics: const BouncingScrollPhysics(), itemBuilder: (context, index) { if (index < inheritedData.memories.length - 1) { final nextFile = inheritedData.memories[index + 1].file; preloadThumbnail(nextFile); preloadFile(nextFile); } + final currentFile = inheritedData.memories[index].file; + final isVideo = currentFile.fileType == FileType.video; return GestureDetector( onTapDown: (TapDownDetails details) { final screenWidth = MediaQuery.of(context).size.width; @@ -262,17 +327,39 @@ class _FullScreenMemoryState extends State { } } }, - child: FileWidget( - inheritedData.memories[index].file, - autoPlay: false, - tagPrefix: "memories", - backgroundDecoration: const BoxDecoration( - color: Colors.transparent, + onLongPress: () { + _toggleAnimation(true); + }, + onLongPressUp: () { + _toggleAnimation(false); + }, + child: MemoriesZoomWidget( + scaleController: (controller) { + _zoomAnimationController = controller; + }, + zoomIn: index % 2 == 0, + isVideo: isVideo, + child: FileWidget( + inheritedData.memories[index].file, + autoPlay: false, + tagPrefix: "memories", + backgroundDecoration: const BoxDecoration( + color: Colors.transparent, + ), + isFromMemories: true, + playbackCallback: (isPlaying) { + isPlaying + ? _toggleAnimation(false) + : _toggleAnimation(true); + }, + onFinalFileLoad: (duration) { + onFinalFileLoad(duration); + }, ), ), ); }, - onPageChanged: (index) { + onPageChanged: (index) async { unawaited( memoriesCacheService.markMemoryAsSeen( inheritedData.memories[index], @@ -450,3 +537,148 @@ class BottomGradient extends StatelessWidget { ); } } + +class MemoryBackDrop extends StatelessWidget { + const MemoryBackDrop({super.key}); + + @override + Widget build(BuildContext context) { + final inheritedData = FullScreenMemoryData.of(context)!; + return ValueListenableBuilder( + valueListenable: inheritedData.indexNotifier, + builder: (context, value, _) { + final currentFile = inheritedData.memories[value].file; + if (currentFile.fileType == FileType.video || + currentFile.fileType == FileType.livePhoto) { + return const SizedBox.shrink(); + } + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + child: Stack( + children: [ + Container( + width: double.infinity, + height: double.infinity, + color: Colors.transparent, + ), + ThumbnailWidget( + currentFile, + shouldShowSyncStatus: false, + shouldShowFavoriteIcon: false, + ), + BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 100, + sigmaY: 100, + ), + child: Container( + color: Colors.transparent, + ), + ), + ], + ), + ); + }, + ); + } +} + +class MemoriesZoomWidget extends StatefulWidget { + final Widget child; + final bool isVideo; + final void Function(AnimationController)? scaleController; + final bool zoomIn; + + const MemoriesZoomWidget({ + super.key, + required this.child, + required this.isVideo, + required this.zoomIn, + this.scaleController, + }); + + @override + State createState() => _MemoriesZoomWidgetState(); +} + +class _MemoriesZoomWidgetState extends State + with TickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + late Animation _panAnimation; + Random random = Random(); + + @override + void initState() { + super.initState(); + _initAnimation(); + } + + void _initAnimation() { + _controller = AnimationController( + vsync: this, + duration: const Duration( + seconds: 5, + ), + ); + + final startScale = widget.zoomIn ? 1.05 : 1.15; + final endScale = widget.zoomIn ? 1.15 : 1.05; + + final startX = (random.nextDouble() - 0.5) * 0.1; + final startY = (random.nextDouble() - 0.5) * 0.1; + final endX = (random.nextDouble() - 0.5) * 0.1; + final endY = (random.nextDouble() - 0.5) * 0.1; + + _scaleAnimation = Tween( + begin: startScale, + end: endScale, + ).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + ), + ); + + _panAnimation = Tween( + begin: Offset(startX, startY), + end: Offset(endX, endY), + ).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + ), + ); + + if (widget.scaleController != null) { + widget.scaleController!(_controller); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.isVideo + ? widget.child + : AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Transform.translate( + offset: Offset( + _panAnimation.value.dx * 100, + _panAnimation.value.dy * 100, + ), + child: widget.child, + ), + ); + }, + ); + } +} diff --git a/mobile/lib/ui/home/memories/memories_widget.dart b/mobile/lib/ui/home/memories/memories_widget.dart index 03625648b7..e3c9f69204 100644 --- a/mobile/lib/ui/home/memories/memories_widget.dart +++ b/mobile/lib/ui/home/memories/memories_widget.dart @@ -130,17 +130,11 @@ class _MemoriesWidgetState extends State { controller: _controller, itemCount: collatedMemories.length, itemBuilder: (context, itemIndex) { - final maxScaleOffsetX = - _maxWidth + MemoryCoverWidget.horizontalPadding * 2; - final offsetOfItem = - (_maxWidth + MemoryCoverWidget.horizontalPadding * 2) * itemIndex; return MemoryCoverWidget( memories: collatedMemories[itemIndex].$1, controller: _controller, - offsetOfItem: offsetOfItem, maxHeight: _maxHeight, maxWidth: _maxWidth, - maxScaleOffsetX: maxScaleOffsetX, title: collatedMemories[itemIndex].$2, ); }, diff --git a/mobile/lib/ui/home/memories/memory_cover_widget.dart b/mobile/lib/ui/home/memories/memory_cover_widget.dart index 55f8db652c..f2754806df 100644 --- a/mobile/lib/ui/home/memories/memory_cover_widget.dart +++ b/mobile/lib/ui/home/memories/memory_cover_widget.dart @@ -11,22 +11,18 @@ import "package:photos/utils/navigation_util.dart"; class MemoryCoverWidget extends StatefulWidget { final List memories; final ScrollController controller; - final double offsetOfItem; final double maxHeight; final double maxWidth; static const outerStrokeWidth = 1.0; static const aspectRatio = 0.68; static const horizontalPadding = 2.5; - final double maxScaleOffsetX; final String title; const MemoryCoverWidget({ required this.memories, required this.controller, - required this.offsetOfItem, required this.maxHeight, required this.maxWidth, - required this.maxScaleOffsetX, required this.title, super.key, }); @@ -44,7 +40,6 @@ class _MemoryCoverWidgetState extends State { return const SizedBox.shrink(); } - final widthOfScreen = MediaQuery.sizeOf(context).width; final index = _getNextMemoryIndex(); final title = widget.title; @@ -56,9 +51,6 @@ class _MemoryCoverWidgetState extends State { return AnimatedBuilder( animation: widget.controller, builder: (context, child) { - final diff = (widget.controller.offset - widget.offsetOfItem) + - widget.maxScaleOffsetX; - final scale = 1 - (diff / widthOfScreen).abs() / 3.7; return Padding( padding: const EdgeInsets.symmetric( horizontal: MemoryCoverWidget.horizontalPadding, @@ -81,8 +73,8 @@ class _MemoryCoverWidgetState extends State { child: Row( children: [ Container( - height: widget.maxHeight * scale, - width: widget.maxWidth * scale, + height: widget.maxHeight , + width: widget.maxWidth , decoration: BoxDecoration( boxShadow: brightness == Brightness.dark ? [ @@ -122,29 +114,26 @@ class _MemoryCoverWidgetState extends State { ), ), Positioned( - bottom: 8 * scale, - child: Transform.scale( - scale: scale, - child: SizedBox( - width: widget.maxWidth, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - ), - child: Hero( - tag: title, - child: Center( - child: Text( - title, - style: getEnteTextTheme(context) - .miniBold - .copyWith( - color: isSeen - ? textFaintDark - : Colors.white, - ), - textAlign: TextAlign.left, - ), + bottom: 8 , + child: SizedBox( + width: widget.maxWidth, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + ), + child: Hero( + tag: title, + child: Center( + child: Text( + title, + style: getEnteTextTheme(context) + .miniBold + .copyWith( + color: isSeen + ? textFaintDark + : Colors.white, + ), + textAlign: TextAlign.left, ), ), ), @@ -173,27 +162,24 @@ class _MemoryCoverWidgetState extends State { ), ), Positioned( - bottom: 8 * scale, - child: Transform.scale( - scale: scale, - child: SizedBox( - width: widget.maxWidth, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - ), - child: Hero( - tag: title, - child: Center( - child: Text( - title, - style: getEnteTextTheme(context) - .miniBold - .copyWith( - color: Colors.white, - ), - textAlign: TextAlign.left, - ), + bottom: 8 , + child: SizedBox( + width: widget.maxWidth, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + ), + child: Hero( + tag: title, + child: Center( + child: Text( + title, + style: getEnteTextTheme(context) + .miniBold + .copyWith( + color: Colors.white, + ), + textAlign: TextAlign.left, ), ), ), diff --git a/mobile/lib/ui/home/memories/memory_progress_indicator.dart b/mobile/lib/ui/home/memories/memory_progress_indicator.dart new file mode 100644 index 0000000000..733acb30c5 --- /dev/null +++ b/mobile/lib/ui/home/memories/memory_progress_indicator.dart @@ -0,0 +1,106 @@ +import "package:flutter/material.dart"; + +class NewProgressIndicator extends StatefulWidget { + final int totalSteps; + final int currentIndex; + final Duration duration; + final Color selectedColor; + final Color unselectedColor; + final double height; + final double gap; + final void Function(AnimationController)? animationController; + final VoidCallback? onComplete; + + const NewProgressIndicator({ + super.key, + required this.totalSteps, + required this.currentIndex, + this.duration = const Duration(seconds: 5), + this.selectedColor = Colors.white, + this.unselectedColor = Colors.white54, + this.height = 2.0, + this.gap = 4.0, + this.animationController, + this.onComplete, + }); + + @override + State createState() => _NewProgressIndicatorState(); +} + +class _NewProgressIndicatorState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _animation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: widget.duration, + ); + + _animation = + Tween(begin: 0.0, end: 1.0).animate(_animationController); + + if (widget.animationController != null) { + widget.animationController!(_animationController); + } + + _animationController.addStatusListener((status) { + if (status == AnimationStatus.completed && widget.onComplete != null) { + widget.onComplete!(); + } + }); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Row( + children: List.generate(widget.totalSteps, (index) { + return Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 4, right: 4), + child: index < widget.currentIndex + ? Container( + height: widget.height, + decoration: BoxDecoration( + color: widget.selectedColor, + borderRadius: BorderRadius.circular(2), + ), + ) + : index == widget.currentIndex + ? AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return LinearProgressIndicator( + value: _animation.value, + backgroundColor: widget.unselectedColor, + valueColor: AlwaysStoppedAnimation( + widget.selectedColor, + ), + minHeight: widget.height, + borderRadius: BorderRadius.circular(2), + ); + }, + ) + : Container( + height: widget.height, + decoration: BoxDecoration( + color: widget.unselectedColor, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + ); + }), + ); + } +} diff --git a/mobile/lib/ui/notification/update/change_log_page.dart b/mobile/lib/ui/notification/update/change_log_page.dart index 2d115b3d88..84b95e3262 100644 --- a/mobile/lib/ui/notification/update/change_log_page.dart +++ b/mobile/lib/ui/notification/update/change_log_page.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import "package:photos/generated/l10n.dart"; -import "package:photos/l10n/l10n.dart"; import "package:photos/service_locator.dart"; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; @@ -38,7 +37,7 @@ class _ChangeLogPageState extends State { padding: EdgeInsets.symmetric(horizontal: 16.0), child: TitleBarTitleWidget( // title: S.of(context).whatsNew, - title: "v1.0", + title: "v1.1.0", ), ), ), @@ -102,24 +101,28 @@ class _ChangeLogPageState extends State { final List items = []; items.addAll([ ChangeLogEntry( - context.l10n.cLIcon, - context.l10n.cLIconDesc, + "On This Day", + "You can now see a new memory called \"On This Day\" showing photos of a date across multiple years. Moreover, you will receive an opt-out notification every morning to check out this new memory.", ), ChangeLogEntry( - context.l10n.cLMemories, - context.l10n.cLMemoriesDesc, + "New Widgets", + "We have added new widgets for albums and people. You can customise which albums or people you want to see in these widgets as well.", ), ChangeLogEntry( - context.l10n.cLWidgets, - context.l10n.cLWidgetsDesc, + "Shareable Favorites", + "You can now share your favorites to your contacts just like any other album.", ), ChangeLogEntry( - context.l10n.cLFamilyPlan, - context.l10n.cLFamilyPlanDesc, + "Albums Improvements", + "We have redesigned the albums screen so you can select multiple albums and take actions like share, hide, archive, delete on all selected albums quickly.", ), ChangeLogEntry( - context.l10n.cLBulkEdit, - context.l10n.cLBulkEditDesc, + "Add to Multiple Albums", + "You can now select multiple albums when you want to add a photo to an album.", + ), + ChangeLogEntry( + "Albums in Contact Page", + "We have updated the contacts page to show you all the albums shared with you by the contact, so you can browse someone's shared library easily.", ), ]); diff --git a/mobile/lib/ui/settings/debug/ml_debug_section_widget.dart b/mobile/lib/ui/settings/debug/ml_debug_section_widget.dart index 7010860b97..a597b94158 100644 --- a/mobile/lib/ui/settings/debug/ml_debug_section_widget.dart +++ b/mobile/lib/ui/settings/debug/ml_debug_section_widget.dart @@ -21,6 +21,7 @@ import "package:photos/services/machine_learning/ml_indexing_isolate.dart"; import 'package:photos/services/machine_learning/ml_service.dart'; import "package:photos/services/machine_learning/semantic_search/semantic_search_service.dart"; import "package:photos/services/memory_home_widget_service.dart"; +import "package:photos/services/notification_service.dart"; import "package:photos/src/rust/api/simple.dart"; import "package:photos/src/rust/api/usearch_api.dart"; import 'package:photos/theme/ente_theme.dart'; @@ -77,6 +78,120 @@ class _MLDebugSectionWidgetState extends State { logger.info("Building ML Debug section options"); return Column( children: [ + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Show pending notifications", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + try { + final amount = + await NotificationService.instance.pendingNotifications(); + showShortToast(context, '$amount pending notifications'); + } catch (e, s) { + logger.severe('pendingNotifications failed ', e, s); + await showGenericErrorDialog(context: context, error: e); + } + }, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Clear pending notifications", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + try { + await NotificationService.instance.clearAllScheduledNotifications( + containingPayload: "onThisDay", + ); + showShortToast(context, 'Done'); + } catch (e, s) { + logger.severe('clearAllScheduledNotifications failed ', e, s); + await showGenericErrorDialog(context: context, error: e); + } + }, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Schedule notification 10 seconds", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + try { + await NotificationService.instance.scheduleNotification( + "test", + "test", + id: 10, + dateTime: DateTime.now().add( + const Duration(seconds: 10), + ), + ); + showShortToast(context, 'done'); + } catch (e, s) { + logger.severe('schedule notification failed ', e, s); + await showGenericErrorDialog(context: context, error: e); + } + }, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Schedule notification 1 hour", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + try { + await NotificationService.instance.scheduleNotification( + "test", + "test", + id: 11, + dateTime: DateTime.now().add( + const Duration(hours: 1), + ), + ); + showShortToast(context, 'done'); + } catch (e, s) { + logger.severe('schedule notification failed ', e, s); + await showGenericErrorDialog(context: context, error: e); + } + }, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Schedule notification 12 hours", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + try { + await NotificationService.instance.scheduleNotification( + "test", + "test", + id: 12, + dateTime: DateTime.now().add( + const Duration(hours: 12), + ), + ); + showShortToast(context, 'done'); + } catch (e, s) { + logger.severe('schedule notification failed ', e, s); + await showGenericErrorDialog(context: context, error: e); + } + }, + ), sectionOptionSpacing, MenuItemWidget( captionedTextWidget: const CaptionedTextWidget( @@ -713,7 +828,7 @@ class _MLDebugSectionWidgetState extends State { trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, onTap: () async => - await MemoryHomeWidgetService.instance.initMemoryHW(true), + await MemoryHomeWidgetService.instance.initMemoryHomeWidget(true), ), sectionOptionSpacing, MenuItemWidget( @@ -723,8 +838,8 @@ class _MLDebugSectionWidgetState extends State { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, - onTap: () async => - await MemoryHomeWidgetService.instance.initMemoryHW(false), + onTap: () async => await MemoryHomeWidgetService.instance + .initMemoryHomeWidget(false), ), sectionOptionSpacing, MenuItemWidget( diff --git a/mobile/lib/ui/settings/developer_settings_page.dart b/mobile/lib/ui/settings/developer_settings_page.dart index 27267bf516..f78e96213a 100644 --- a/mobile/lib/ui/settings/developer_settings_page.dart +++ b/mobile/lib/ui/settings/developer_settings_page.dart @@ -65,7 +65,7 @@ class _DeveloperSettingsPageState extends State { showErrorDialog( context, S.of(context).invalidEndpoint, - S.of(context).invalidEndpointMessage, + S.of(context).invalidEndpointMessage + "\n" + e.toString(), ); } }, diff --git a/mobile/lib/ui/settings/gallery_settings_screen.dart b/mobile/lib/ui/settings/gallery_settings_screen.dart index 3e4e86e1d0..2fc5864cc3 100644 --- a/mobile/lib/ui/settings/gallery_settings_screen.dart +++ b/mobile/lib/ui/settings/gallery_settings_screen.dart @@ -109,7 +109,7 @@ class _GallerySettingsScreenState extends State { ); unawaited( MemoryHomeWidgetService.instance - .initMemoryHW(true), + .initMemoryHomeWidget(true), ); setState(() {}); }, diff --git a/mobile/lib/ui/settings/general_section_widget.dart b/mobile/lib/ui/settings/general_section_widget.dart index b5f7e35ad3..e4f5df18cc 100644 --- a/mobile/lib/ui/settings/general_section_widget.dart +++ b/mobile/lib/ui/settings/general_section_widget.dart @@ -15,6 +15,7 @@ import 'package:photos/ui/settings/advanced_settings_screen.dart'; import 'package:photos/ui/settings/common_settings.dart'; import "package:photos/ui/settings/language_picker.dart"; import "package:photos/ui/settings/notification_settings_screen.dart"; +import "package:photos/ui/settings/widget_settings_screen.dart"; import 'package:photos/utils/navigation_util.dart'; class GeneralSectionWidget extends StatelessWidget { @@ -45,7 +46,6 @@ class GeneralSectionWidget extends StatelessWidget { routeToPage( context, const ReferralScreen(), - forceCustomPageRoute: true, ); }, ), @@ -98,6 +98,18 @@ class GeneralSectionWidget extends StatelessWidget { }, ), sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).widgets, + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + _onWidgetsTapped(context); + }, + ), + sectionOptionSpacing, MenuItemWidget( captionedTextWidget: CaptionedTextWidget( title: S.of(context).advanced, @@ -128,6 +140,13 @@ class GeneralSectionWidget extends StatelessWidget { ); } + void _onWidgetsTapped(BuildContext context) { + routeToPage( + context, + const WidgetSettingsScreen(), + ); + } + void _onAdvancedTapped(BuildContext context) { routeToPage( context, diff --git a/mobile/lib/ui/settings/language_picker.dart b/mobile/lib/ui/settings/language_picker.dart index 86d9a9a674..009150afb9 100644 --- a/mobile/lib/ui/settings/language_picker.dart +++ b/mobile/lib/ui/settings/language_picker.dart @@ -98,11 +98,21 @@ class _ItemsWidgetState extends State { @override Widget build(BuildContext context) { items.clear(); + bool foundMatch = false; for (Locale locale in widget.supportedLocales) { + if (currentLocale == locale) { + foundMatch = true; + } items.add( _menuItemForPicker(locale), ); } + if (!foundMatch && kDebugMode) { + items.insert( + 0, + Text("(i) Locale : ${currentLocale.toString()}"), + ); + } items = addSeparators( items, DividerWidget( diff --git a/mobile/lib/ui/settings/notification_settings_screen.dart b/mobile/lib/ui/settings/notification_settings_screen.dart index f6dfc3eabc..de4df89574 100644 --- a/mobile/lib/ui/settings/notification_settings_screen.dart +++ b/mobile/lib/ui/settings/notification_settings_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import "package:photos/generated/l10n.dart"; +import "package:photos/service_locator.dart"; import "package:photos/services/notification_service.dart"; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/buttons/icon_button_widget.dart'; @@ -9,6 +10,7 @@ import 'package:photos/ui/components/menu_section_description_widget.dart'; import 'package:photos/ui/components/title_bar_title_widget.dart'; import 'package:photos/ui/components/title_bar_widget.dart'; import 'package:photos/ui/components/toggle_switch_widget.dart'; +import "package:photos/ui/settings/common_settings.dart"; class NotificationSettingsScreen extends StatelessWidget { const NotificationSettingsScreen({super.key}); @@ -77,6 +79,33 @@ class NotificationSettingsScreen extends StatelessWidget { .of(context) .sharedPhotoNotificationsExplanation, ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).onThisDayMemories, + ), + menuItemColor: colorScheme.fillFaint, + trailingWidget: ToggleSwitchWidget( + value: () => + NotificationService.instance + .hasGrantedPermissions() && + localSettings + .isOnThisDayNotificationsEnabled, + onChanged: () async { + await NotificationService.instance + .requestPermissions(); + await memoriesCacheService + .toggleOnThisDayNotifications(); + }, + ), + singleBorderRadius: 8, + alignCaptionedTextToLeft: true, + isGestureDetectorDisabled: true, + ), + MenuSectionDescriptionWidget( + content: + S.of(context).onThisDayNotificationExplanation, + ), ], ), ], diff --git a/mobile/lib/ui/settings/pending_sync/pending_sync_info_screen.dart b/mobile/lib/ui/settings/pending_sync/pending_sync_info_screen.dart index e00dce529b..4c51486d57 100644 --- a/mobile/lib/ui/settings/pending_sync/pending_sync_info_screen.dart +++ b/mobile/lib/ui/settings/pending_sync/pending_sync_info_screen.dart @@ -82,6 +82,12 @@ class _PendingSyncInfoScreenState extends State { ".decrypted", allowCacheClear: false, ), + PathInfoStorageItem.name( + tempDownload, + "Partial Download", + "_part", + allowCacheClear: true, + ), PathInfoStorageItem.name( tempDownload, "Video Preview", diff --git a/mobile/lib/ui/settings/widget_settings_screen.dart b/mobile/lib/ui/settings/widget_settings_screen.dart new file mode 100644 index 0000000000..c5d491397f --- /dev/null +++ b/mobile/lib/ui/settings/widget_settings_screen.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import "package:flutter_svg/flutter_svg.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/service_locator.dart"; +import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; +import 'package:photos/ui/components/captioned_text_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; +import 'package:photos/ui/components/title_bar_title_widget.dart'; +import 'package:photos/ui/components/title_bar_widget.dart'; +import "package:photos/ui/settings/ml/enable_ml_consent.dart"; +import "package:photos/ui/settings/widgets/albums_widget_settings.dart"; +import "package:photos/ui/settings/widgets/memories_widget_settings.dart"; +import "package:photos/ui/settings/widgets/people_widget_settings.dart"; +import "package:photos/utils/navigation_util.dart"; + +class WidgetSettingsScreen extends StatelessWidget { + const WidgetSettingsScreen({super.key}); + + void onPeopleTapped(BuildContext context) { + final bool isMLEnabled = !flagService.hasGrantedMLConsent; + if (isMLEnabled) { + routeToPage( + context, + const EnableMachineLearningConsent(), + forceCustomPageRoute: true, + ); + return; + } + routeToPage( + context, + const PeopleWidgetSettings(), + ); + } + + void onAlbumsTapped(BuildContext context) { + routeToPage( + context, + const AlbumsWidgetSettings(), + ); + } + + void onMemoriesTapped(BuildContext context) { + routeToPage( + context, + const MemoriesWidgetSettings(), + ); + } + + @override + Widget build(BuildContext context) { + final colorScheme = getEnteColorScheme(context); + return Scaffold( + body: CustomScrollView( + primary: false, + slivers: [ + TitleBarWidget( + flexibleSpaceTitle: TitleBarTitleWidget( + title: S.of(context).widgets, + ), + actionIcons: [ + IconButtonWidget( + icon: Icons.close_outlined, + iconButtonType: IconButtonType.secondary, + onTap: () { + Navigator.pop(context); + Navigator.pop(context); + }, + ), + ], + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + children: [ + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).people, + ), + leadingIconWidget: SvgPicture.asset( + "assets/icons/people-widget-icon.svg", + color: colorScheme.textBase, + ), + menuItemColor: colorScheme.fillFaint, + singleBorderRadius: 8, + trailingIcon: Icons.chevron_right_outlined, + onTap: () async => onPeopleTapped(context), + ), + const SizedBox(height: 8), + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).albums, + ), + leadingIconWidget: SvgPicture.asset( + "assets/icons/albums-widget-icon.svg", + color: colorScheme.textBase, + ), + menuItemColor: colorScheme.fillFaint, + singleBorderRadius: 8, + trailingIcon: Icons.chevron_right_outlined, + onTap: () async => onAlbumsTapped(context), + ), + const SizedBox(height: 8), + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).memories, + ), + leadingIconWidget: SvgPicture.asset( + "assets/icons/memories-widget-icon.svg", + color: colorScheme.textBase, + ), + menuItemColor: colorScheme.fillFaint, + singleBorderRadius: 8, + trailingIcon: Icons.chevron_right_outlined, + onTap: () async => onMemoriesTapped(context), + ), + ], + ), + ], + ), + ), + ); + }, + childCount: 1, + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/ui/settings/widgets/albums_widget_settings.dart b/mobile/lib/ui/settings/widgets/albums_widget_settings.dart new file mode 100644 index 0000000000..03ec9e9b42 --- /dev/null +++ b/mobile/lib/ui/settings/widgets/albums_widget_settings.dart @@ -0,0 +1,199 @@ +import 'package:flutter/material.dart'; +import "package:photos/generated/l10n.dart"; +import "package:photos/l10n/l10n.dart"; +import "package:photos/models/collection/collection.dart"; +import "package:photos/models/selected_albums.dart"; +import "package:photos/services/album_home_widget_service.dart"; +import "package:photos/services/collections_service.dart"; +import "package:photos/services/favorites_service.dart"; +import "package:photos/ui/collections/flex_grid_view.dart"; +import "package:photos/ui/common/loading_widget.dart"; +import "package:photos/ui/components/buttons/button_widget.dart"; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; +import "package:photos/ui/components/models/button_type.dart"; +import 'package:photos/ui/components/title_bar_title_widget.dart'; +import 'package:photos/ui/components/title_bar_widget.dart'; + +class AlbumsWidgetSettings extends StatefulWidget { + const AlbumsWidgetSettings({super.key}); + + @override + State createState() => _AlbumsWidgetSettingsState(); +} + +class _AlbumsWidgetSettingsState extends State { + final _selectedAlbums = SelectedAlbums(); + bool hasInstalledAny = false; + final _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + checkIfAnyWidgetInstalled(); + selectExisting(); + } + + Future checkIfAnyWidgetInstalled() async { + final count = await AlbumHomeWidgetService.instance.countHomeWidgets(); + setState(() { + hasInstalledAny = count > 0; + }); + } + + Future selectExisting() async { + final selectedAlbums = + AlbumHomeWidgetService.instance.getSelectedAlbumIds(); + final albums = {}; + + if (selectedAlbums != null) { + for (final collectionID in selectedAlbums) { + final collection = + CollectionsService.instance.getCollectionByID(collectionID); + + if (collection != null) { + albums.add(collection); + } + } + } + + if (albums.isEmpty) { + final favorites = + await FavoritesService.instance.getFavoritesCollection(); + + if (favorites == null) { + return; + } + + albums.add(favorites); + } + + _selectedAlbums.select(albums); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + bottomNavigationBar: hasInstalledAny + ? Padding( + padding: EdgeInsets.fromLTRB( + 16, + 8, + 16, + 8 + MediaQuery.viewPaddingOf(context).bottom, + ), + child: ListenableBuilder( + listenable: _selectedAlbums, + builder: (context, _) { + return ButtonWidget( + buttonType: ButtonType.primary, + buttonSize: ButtonSize.large, + labelText: S.of(context).save, + shouldSurfaceExecutionStates: false, + onTap: _selectedAlbums.albums.isNotEmpty + ? () async { + final albums = _selectedAlbums.albums + .map((e) => e.id.toString()) + .toList(); + await AlbumHomeWidgetService.instance + .setSelectedAlbums(albums); + Navigator.pop(context); + await AlbumHomeWidgetService.instance + .albumsChanged(); + } + : null, + isDisabled: _selectedAlbums.albums.isEmpty, + ); + }, + ), + ) + : null, + body: Scrollbar( + interactive: true, + controller: _scrollController, + child: CustomScrollView( + controller: _scrollController, + primary: false, + slivers: [ + TitleBarWidget( + flexibleSpaceTitle: TitleBarTitleWidget( + title: S.of(context).albums, + ), + expandedHeight: 120, + flexibleSpaceCaption: hasInstalledAny + ? S.of(context).albumsWidgetDesc + : context.l10n.addAlbumWidgetPrompt, + actionIcons: [ + IconButtonWidget( + icon: Icons.close_outlined, + iconButtonType: IconButtonType.secondary, + onTap: () { + Navigator.pop(context); + Navigator.pop(context); + Navigator.pop(context); + }, + ), + ], + ), + if (!hasInstalledAny) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: MediaQuery.sizeOf(context).height * 0.5 - 200, + ), + Image.asset( + "assets/albums-widget-static.png", + height: 160, + ), + ], + ), + ), + ) + else + FutureBuilder>( + future: + CollectionsService.instance.getCollectionForOnEnteSection(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final data = snapshot.data!; + for (final collection in snapshot.data!) { + if (_selectedAlbums.albums.contains(collection)) { + data.remove(collection); + data.insert(0, collection); + } + } + + return CollectionsFlexiGridViewWidget( + data, + displayLimitCount: snapshot.data!.length, + shrinkWrap: false, + selectedAlbums: _selectedAlbums, + shouldShowCreateAlbum: false, + enableSelectionMode: true, + onlyAllowSelection: true, + ); + } else if (snapshot.hasError) { + return SliverToBoxAdapter( + child: Text(snapshot.error.toString()), + ); + } else { + return const SliverToBoxAdapter(child: EnteLoadingWidget()); + } + }, + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/ui/settings/widgets/memories_widget_settings.dart b/mobile/lib/ui/settings/widgets/memories_widget_settings.dart new file mode 100644 index 0000000000..51a0d6310c --- /dev/null +++ b/mobile/lib/ui/settings/widgets/memories_widget_settings.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import "package:flutter_svg/flutter_svg.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/l10n/l10n.dart"; +import "package:photos/service_locator.dart"; +import "package:photos/services/memory_home_widget_service.dart"; +import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; +import "package:photos/ui/components/captioned_text_widget.dart"; +import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart"; +import 'package:photos/ui/components/title_bar_title_widget.dart'; +import 'package:photos/ui/components/title_bar_widget.dart'; +import "package:photos/ui/components/toggle_switch_widget.dart"; + +class MemoriesWidgetSettings extends StatefulWidget { + const MemoriesWidgetSettings({super.key}); + + @override + State createState() => _MemoriesWidgetSettingsState(); +} + +class _MemoriesWidgetSettingsState extends State { + bool hasInstalledAny = false; + + bool? isYearlyMemoriesEnabled = true; + bool? isSmartMemoriesEnabled = false; + bool? isOnThisDayMemoriesEnabled = false; + + late final bool isMLEnabled; + + @override + void initState() { + super.initState(); + + initVariables(); + checkIfAnyWidgetInstalled(); + } + + Future checkIfAnyWidgetInstalled() async { + final count = await MemoryHomeWidgetService.instance.countHomeWidgets(); + setState(() { + hasInstalledAny = count > 0; + }); + } + + Future initVariables() async { + isMLEnabled = flagService.hasGrantedMLConsent; + isYearlyMemoriesEnabled = + await MemoryHomeWidgetService.instance.getSelectedLastYearMemories(); + isSmartMemoriesEnabled = + await MemoryHomeWidgetService.instance.getSelectedMLMemories(); + isOnThisDayMemoriesEnabled = + await MemoryHomeWidgetService.instance.getSelectedOnThisDayMemories(); + + if (isYearlyMemoriesEnabled == null || + isSmartMemoriesEnabled == null || + isOnThisDayMemoriesEnabled == null) { + if (isMLEnabled) { + enableMLMemories(); + } else { + enableNonMLMemories(); + } + } + + setState(() {}); + } + + void enableMLMemories() { + isYearlyMemoriesEnabled = false; + isSmartMemoriesEnabled = true; + isOnThisDayMemoriesEnabled = false; + } + + void enableNonMLMemories() { + isYearlyMemoriesEnabled = true; + isSmartMemoriesEnabled = false; + isOnThisDayMemoriesEnabled = true; + } + + Future updateVariables() async { + await MemoryHomeWidgetService.instance + .setSelectedLastYearMemories(isYearlyMemoriesEnabled!); + await MemoryHomeWidgetService.instance + .setSelectedMLMemories(isSmartMemoriesEnabled!); + await MemoryHomeWidgetService.instance + .setSelectedOnThisDayMemories(isOnThisDayMemoriesEnabled!); + await MemoryHomeWidgetService.instance.memoryChanged(); + } + + @override + Widget build(BuildContext context) { + final colorScheme = getEnteColorScheme(context); + + return Scaffold( + body: CustomScrollView( + primary: false, + slivers: [ + TitleBarWidget( + flexibleSpaceTitle: TitleBarTitleWidget( + title: S.of(context).memories, + ), + expandedHeight: 120, + flexibleSpaceCaption: hasInstalledAny + ? S.of(context).memoriesWidgetDesc + : context.l10n.addMemoriesWidgetPrompt, + actionIcons: [ + IconButtonWidget( + icon: Icons.close_outlined, + iconButtonType: IconButtonType.secondary, + onTap: () { + Navigator.pop(context); + Navigator.pop(context); + Navigator.pop(context); + }, + ), + ], + ), + if (!hasInstalledAny) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: MediaQuery.sizeOf(context).height * 0.5 - 200, + ), + Image.asset( + "assets/memories-widget-static.png", + height: 160, + ), + ], + ), + ), + ) + else + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 18), + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).pastYearsMemories, + ), + leadingIconWidget: SvgPicture.asset( + "assets/icons/past-year-memory-icon.svg", + color: colorScheme.textBase, + ), + menuItemColor: colorScheme.fillFaint, + trailingWidget: ToggleSwitchWidget( + value: () => isYearlyMemoriesEnabled ?? true, + onChanged: () async { + setState(() { + isYearlyMemoriesEnabled = + !isYearlyMemoriesEnabled!; + }); + await updateVariables(); + }, + ), + singleBorderRadius: 8, + isGestureDetectorDisabled: true, + ), + const SizedBox(height: 4), + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).onThisDayMemories, + ), + leadingIconWidget: SvgPicture.asset( + "assets/icons/memories-widget-icon.svg", + color: colorScheme.textBase, + ), + menuItemColor: colorScheme.fillFaint, + trailingWidget: ToggleSwitchWidget( + value: () => isOnThisDayMemoriesEnabled!, + onChanged: () async { + setState(() { + isOnThisDayMemoriesEnabled = + !isOnThisDayMemoriesEnabled!; + }); + await updateVariables(); + }, + ), + singleBorderRadius: 8, + isGestureDetectorDisabled: true, + ), + if (isMLEnabled) ...[ + const SizedBox(height: 4), + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).smartMemories, + ), + leadingIconWidget: SvgPicture.asset( + "assets/icons/smart-memory-icon.svg", + color: colorScheme.textBase, + ), + menuItemColor: colorScheme.fillFaint, + trailingWidget: ToggleSwitchWidget( + value: () => isSmartMemoriesEnabled!, + onChanged: () async { + setState(() { + isSmartMemoriesEnabled = + !isSmartMemoriesEnabled!; + }); + await updateVariables(); + }, + ), + singleBorderRadius: 8, + isGestureDetectorDisabled: true, + ), + ], + ], + ), + ); + }, + childCount: 1, + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/ui/settings/widgets/people_widget_settings.dart b/mobile/lib/ui/settings/widgets/people_widget_settings.dart new file mode 100644 index 0000000000..760d7df100 --- /dev/null +++ b/mobile/lib/ui/settings/widgets/people_widget_settings.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; +import "package:photos/generated/l10n.dart"; +import "package:photos/l10n/l10n.dart"; +import "package:photos/models/selected_people.dart"; +import "package:photos/services/people_home_widget_service.dart"; +import "package:photos/ui/components/buttons/button_widget.dart"; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; +import "package:photos/ui/components/models/button_type.dart"; +import 'package:photos/ui/components/title_bar_title_widget.dart'; +import 'package:photos/ui/components/title_bar_widget.dart'; +import "package:photos/ui/viewer/search/result/people_section_all_page.dart"; + +class PeopleWidgetSettings extends StatefulWidget { + const PeopleWidgetSettings({super.key}); + + @override + State createState() => _PeopleWidgetSettingsState(); +} + +class _PeopleWidgetSettingsState extends State { + bool hasInstalledAny = false; + final _selectedPeople = SelectedPeople(); + + @override + void initState() { + super.initState(); + getSelections(); + checkIfAnyWidgetInstalled(); + } + + Future getSelections() async { + final selectedPeople = PeopleHomeWidgetService.instance.getSelectedPeople(); + + if (selectedPeople != null) { + _selectedPeople.select(selectedPeople.toSet()); + } + } + + Future checkIfAnyWidgetInstalled() async { + final count = await PeopleHomeWidgetService.instance.countHomeWidgets(); + setState(() { + hasInstalledAny = count > 0; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + bottomNavigationBar: hasInstalledAny + ? Padding( + padding: EdgeInsets.fromLTRB( + 16, + 8, + 16, + 8 + MediaQuery.viewPaddingOf(context).bottom, + ), + child: ListenableBuilder( + listenable: _selectedPeople, + builder: (context, _) { + return ButtonWidget( + buttonType: ButtonType.primary, + buttonSize: ButtonSize.large, + labelText: S.of(context).save, + shouldSurfaceExecutionStates: false, + isDisabled: _selectedPeople.personIds.isEmpty, + onTap: _selectedPeople.personIds.isEmpty + ? null + : () async { + await PeopleHomeWidgetService.instance + .setSelectedPeople( + _selectedPeople.personIds.toList(), + ); + Navigator.pop(context); + await PeopleHomeWidgetService.instance + .peopleChanged(); + }, + ); + }, + ), + ) + : null, + body: CustomScrollView( + primary: false, + slivers: [ + TitleBarWidget( + flexibleSpaceTitle: TitleBarTitleWidget( + title: S.of(context).people, + ), + expandedHeight: 120, + flexibleSpaceCaption: hasInstalledAny + ? S.of(context).peopleWidgetDesc + : context.l10n.addPeopleWidgetPrompt, + actionIcons: [ + IconButtonWidget( + icon: Icons.close_outlined, + iconButtonType: IconButtonType.secondary, + onTap: () { + Navigator.pop(context); + Navigator.pop(context); + Navigator.pop(context); + }, + ), + ], + ), + if (!hasInstalledAny) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: MediaQuery.sizeOf(context).height * 0.5 - 200, + ), + Image.asset( + "assets/people-widget-static.png", + height: 160, + ), + ], + ), + ), + ) + else + SliverToBoxAdapter( + child: PeopleSectionAllWidget( + selectedPeople: _selectedPeople, + namedOnly: true, + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/ui/sharing/add_participant_page.dart b/mobile/lib/ui/sharing/add_participant_page.dart index 6c2ffbef0d..c16931b121 100644 --- a/mobile/lib/ui/sharing/add_participant_page.dart +++ b/mobile/lib/ui/sharing/add_participant_page.dart @@ -18,12 +18,26 @@ import 'package:photos/ui/components/models/button_type.dart'; import "package:photos/ui/notification/toast.dart"; import 'package:photos/ui/sharing/user_avator_widget.dart'; import "package:photos/ui/sharing/verify_identity_dialog.dart"; +import "package:photos/utils/separators_util.dart"; + +enum ActionTypesToShow { + addViewer, + addCollaborator, +} class AddParticipantPage extends StatefulWidget { - final Collection collection; - final bool isAddingViewer; + /// Cannot be empty + final List actionTypesToShow; + final List collections; - const AddParticipantPage(this.collection, this.isAddingViewer, {super.key}); + AddParticipantPage( + this.collections, + this.actionTypesToShow, { + super.key, + }) : assert( + actionTypesToShow.isNotEmpty, + 'actionTypesToShow cannot be empty', + ); @override State createState() => _AddParticipantPage(); @@ -72,9 +86,7 @@ class _AddParticipantPage extends State { resizeToAvoidBottomInset: isKeypadOpen, appBar: AppBar( title: Text( - widget.isAddingViewer - ? S.of(context).addViewer - : S.of(context).addCollaborator, + _getTitle(), ), ), body: Column( @@ -124,13 +136,15 @@ class _AddParticipantPage extends State { .longPressAnEmailToVerifyEndToEndEncryption, ) : const SizedBox.shrink(), - widget.isAddingViewer - ? const SizedBox.shrink() - : MenuSectionDescriptionWidget( + widget.actionTypesToShow.contains( + ActionTypesToShow.addCollaborator, + ) + ? MenuSectionDescriptionWidget( content: S .of(context) .collaboratorsCanAddPhotosAndVideosToTheSharedAlbum, - ), + ) + : const SizedBox.shrink(), ], ), ); @@ -216,48 +230,7 @@ class _AddParticipantPage extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(height: 8), - ButtonWidget( - buttonType: ButtonType.primary, - buttonSize: ButtonSize.large, - labelText: widget.isAddingViewer - ? S.of(context).addViewers(_selectedEmails.length) - : S - .of(context) - .addCollaborators(_selectedEmails.length), - isDisabled: _selectedEmails.isEmpty, - onTap: () async { - final results = []; - for (String email in _selectedEmails) { - results.add( - await collectionActions.addEmailToCollection( - context, - widget.collection, - email, - widget.isAddingViewer - ? CollectionParticipantRole.viewer - : CollectionParticipantRole.collaborator, - ), - ); - } - - final noOfSuccessfullAdds = - results.where((e) => e).length; - showToast( - context, - widget.isAddingViewer - ? S - .of(context) - .viewersSuccessfullyAdded(noOfSuccessfullAdds) - : S.of(context).collaboratorsSuccessfullyAdded( - noOfSuccessfullAdds, - ), - ); - - if (!results.any((e) => e == false) && mounted) { - Navigator.of(context).pop(true); - } - }, - ), + ..._actionButtons(), const SizedBox(height: 12), ], ), @@ -268,6 +241,100 @@ class _AddParticipantPage extends State { ); } + List _actionButtons() { + final widgets = []; + if (widget.actionTypesToShow.contains(ActionTypesToShow.addViewer)) { + widgets.add( + ButtonWidget( + buttonType: ButtonType.primary, + buttonSize: ButtonSize.large, + labelText: S.of(context).addViewers(_selectedEmails.length), + isDisabled: _selectedEmails.isEmpty, + onTap: () async { + final results = []; + final collections = widget.collections; + + for (String email in _selectedEmails) { + bool result = false; + for (Collection collection in collections) { + result = await collectionActions.addEmailToCollection( + context, + collection, + email, + CollectionParticipantRole.viewer, + ); + } + results.add(result); + } + + final noOfSuccessfullAdds = results.where((e) => e).length; + showToast( + context, + S.of(context).viewersSuccessfullyAdded(noOfSuccessfullAdds), + ); + + if (!results.any((e) => e == false) && mounted) { + Navigator.of(context).pop(true); + } + }, + ), + ); + } + if (widget.actionTypesToShow.contains( + ActionTypesToShow.addCollaborator, + )) { + widgets.add( + ButtonWidget( + buttonType: + widget.actionTypesToShow.contains(ActionTypesToShow.addViewer) + ? ButtonType.neutral + : ButtonType.primary, + buttonSize: ButtonSize.large, + labelText: S.of(context).addCollaborators(_selectedEmails.length), + isDisabled: _selectedEmails.isEmpty, + onTap: () async { + // TODO: This is not currently designed for best UX for action on + // multiple collections and emails, especially if some operations + // fail. Can be improved by using a different 'addEmailToCollection' + // that accepts list of emails and list of collections. + final results = []; + final collections = widget.collections; + + for (String email in _selectedEmails) { + bool result = false; + for (Collection collection in collections) { + result = await collectionActions.addEmailToCollection( + context, + collection, + email, + CollectionParticipantRole.collaborator, + ); + } + results.add(result); + } + + final noOfSuccessfullAdds = results.where((e) => e).length; + showToast( + context, + S.of(context).collaboratorsSuccessfullyAdded(noOfSuccessfullAdds), + ); + + if (!results.any((e) => e == false) && mounted) { + Navigator.of(context).pop(true); + } + }, + ), + ); + } + final widgetsWithSpaceBetween = addSeparators( + widgets, + const SizedBox( + height: 8, + ), + ); + return widgetsWithSpaceBetween; + } + void clearFocus() { _textController.clear(); _newEmail = _textController.text; @@ -363,9 +430,16 @@ class _AddParticipantPage extends State { List _getSuggestedUser() { final Set existingEmails = {}; - for (final User u in widget.collection.sharees) { - if (u.id != null && u.email.isNotEmpty) { - existingEmails.add(u.email); + final collections = widget.collections; + if (collections.isEmpty) { + return []; + } + + for (final Collection collection in collections) { + for (final User u in collection.sharees) { + if (u.id != null && u.email.isNotEmpty) { + existingEmails.add(u.email); + } } } @@ -385,4 +459,14 @@ class _AddParticipantPage extends State { return suggestedUsers; } + + String _getTitle() { + if (widget.actionTypesToShow.length > 1) { + return S.of(context).addParticipants; + } else if (widget.actionTypesToShow.first == ActionTypesToShow.addViewer) { + return S.of(context).addViewer; + } else { + return S.of(context).addCollaborator; + } + } } diff --git a/mobile/lib/ui/sharing/album_participants_page.dart b/mobile/lib/ui/sharing/album_participants_page.dart index e27fe92478..b162b1f8dd 100644 --- a/mobile/lib/ui/sharing/album_participants_page.dart +++ b/mobile/lib/ui/sharing/album_participants_page.dart @@ -54,7 +54,12 @@ class _AlbumParticipantsPageState extends State { Future _navigateToAddUser(bool addingViewer) async { await routeToPage( context, - AddParticipantPage(widget.collection, addingViewer), + AddParticipantPage( + [widget.collection], + addingViewer + ? [ActionTypesToShow.addViewer] + : [ActionTypesToShow.addCollaborator], + ), ); if (mounted) { setState(() => {}); diff --git a/mobile/lib/ui/sharing/share_collection_page.dart b/mobile/lib/ui/sharing/share_collection_page.dart index 64090528c8..0b1ab33bb9 100644 --- a/mobile/lib/ui/sharing/share_collection_page.dart +++ b/mobile/lib/ui/sharing/share_collection_page.dart @@ -92,7 +92,10 @@ class _ShareCollectionPageState extends State { // ignore: unawaited_futures routeToPage( context, - AddParticipantPage(widget.collection, true), + AddParticipantPage( + [widget.collection], + const [ActionTypesToShow.addViewer], + ), ).then( (value) => { if (mounted) {setState(() => {})}, @@ -118,8 +121,13 @@ class _ShareCollectionPageState extends State { isTopBorderRadiusRemoved: true, onTap: () async { // ignore: unawaited_futures - routeToPage(context, AddParticipantPage(widget.collection, false)) - .then( + routeToPage( + context, + AddParticipantPage( + [widget.collection], + const [ActionTypesToShow.addCollaborator], + ), + ).then( (value) => { if (mounted) {setState(() => {})}, }, diff --git a/mobile/lib/ui/sharing/user_avator_widget.dart b/mobile/lib/ui/sharing/user_avator_widget.dart index 7acd869f87..b77ba2d2a0 100644 --- a/mobile/lib/ui/sharing/user_avator_widget.dart +++ b/mobile/lib/ui/sharing/user_avator_widget.dart @@ -8,11 +8,10 @@ import "package:photos/core/event_bus.dart"; import "package:photos/events/people_changed_event.dart"; import "package:photos/extensions/user_extension.dart"; import "package:photos/models/api/collection/user.dart"; -import "package:photos/models/file/file.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import "package:photos/theme/colors.dart"; import 'package:photos/theme/ente_theme.dart'; -import "package:photos/ui/viewer/search/result/person_face_widget.dart"; +import "package:photos/ui/viewer/people/person_face_widget.dart"; import "package:photos/utils/standalone/debouncer.dart"; import 'package:tuple/tuple.dart'; @@ -39,7 +38,7 @@ class UserAvatarWidget extends StatefulWidget { class _UserAvatarWidgetState extends State { Future? _personID; - EnteFile? _faceThumbnail; + bool _canUsePersonFaceWidget = false; final _logger = Logger("_UserAvatarWidgetState"); late final StreamSubscription _peopleChangedSubscription; final _debouncer = Debouncer( @@ -78,10 +77,7 @@ class _UserAvatarWidgetState extends State { final person = people.firstWhereOrNull( (person) => person.data.email == widget.user.email, ); - if (person != null) { - _faceThumbnail = - await PersonService.instance.getThumbnailFileOfPerson(person); - } + _canUsePersonFaceWidget = person != null; return person?.remoteID; }); } else { @@ -117,24 +113,23 @@ class _UserAvatarWidgetState extends State { if (snapshot.hasData) { final personID = snapshot.data as String; return ClipOval( - child: _faceThumbnail == null - ? _FirstLetterCircularAvatar( - user: widget.user, - currentUserID: widget.currentUserID, - thumbnailView: widget.thumbnailView, - type: widget.type, - ) - : PersonFaceWidget( - _faceThumbnail!, + child: _canUsePersonFaceWidget + ? PersonFaceWidget( personId: personID, onErrorCallback: () { if (mounted) { setState(() { _personID = null; - _faceThumbnail = null; + _canUsePersonFaceWidget = false; }); } }, + ) + : _FirstLetterCircularAvatar( + user: widget.user, + currentUserID: widget.currentUserID, + thumbnailView: widget.thumbnailView, + type: widget.type, ), ); } else if (snapshot.hasError) { diff --git a/mobile/lib/ui/tabs/home_widget.dart b/mobile/lib/ui/tabs/home_widget.dart index 1e74fb26d5..7414637e70 100644 --- a/mobile/lib/ui/tabs/home_widget.dart +++ b/mobile/lib/ui/tabs/home_widget.dart @@ -32,14 +32,17 @@ import "package:photos/l10n/l10n.dart"; import "package:photos/models/collection/collection.dart"; import 'package:photos/models/collection/collection_items.dart'; import "package:photos/models/file/file.dart"; +import "package:photos/models/selected_albums.dart"; import 'package:photos/models/selected_files.dart'; import "package:photos/service_locator.dart"; import 'package:photos/services/account/user_service.dart'; +import "package:photos/services/album_home_widget_service.dart"; import 'package:photos/services/app_lifecycle_service.dart'; import 'package:photos/services/collections_service.dart'; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import "package:photos/services/memory_home_widget_service.dart"; import "package:photos/services/notification_service.dart"; +import "package:photos/services/people_home_widget_service.dart"; import "package:photos/services/sync/diff_fetcher.dart"; import 'package:photos/services/sync/local_sync_service.dart'; import "package:photos/services/sync/remote_sync_service.dart"; @@ -83,7 +86,6 @@ class HomeWidget extends StatefulWidget { } class _HomeWidgetState extends State { - static const _userCollectionsTab = UserCollectionsTab(); static const _sharedCollectionTab = SharedCollectionsTab(); static const _searchTab = SearchTab(); static final _settingsPage = SettingsPage( @@ -91,6 +93,7 @@ class _HomeWidgetState extends State { ); final _logger = Logger("HomeWidgetState"); + final _selectedAlbums = SelectedAlbums(); final _selectedFiles = SelectedFiles(); final PageController _pageController = PageController(); @@ -125,6 +128,8 @@ class _HomeWidgetState extends State { if (LocalSyncService.instance.hasCompletedFirstImport()) { MemoryHomeWidgetService.instance.checkPendingMemorySync(); + PeopleHomeWidgetService.instance.checkPendingPeopleSync(); + AlbumHomeWidgetService.instance.checkPendingAlbumsSync(); } _tabChangedEventSubscription = Bus.instance.on().listen((event) { @@ -189,6 +194,8 @@ class _HomeWidgetState extends State { if (mounted) { setState(() {}); MemoryHomeWidgetService.instance.checkPendingMemorySync(); + AlbumHomeWidgetService.instance.checkPendingAlbumsSync(); + PeopleHomeWidgetService.instance.checkPendingPeopleSync(); } }, ); @@ -709,7 +716,7 @@ class _HomeWidgetState extends State { ), selectedFiles: _selectedFiles, ), - _userCollectionsTab, + UserCollectionsTab(selectedAlbums: _selectedAlbums), _sharedCollectionTab, _searchTab, ], @@ -756,6 +763,7 @@ class _HomeWidgetState extends State { : const SizedBox.shrink(), HomeBottomNavigationBar( _selectedFiles, + _selectedAlbums, selectedTabIndex: _selectedTabIndex, ), ], @@ -853,22 +861,27 @@ class _HomeWidgetState extends State { final String? payload = notificationResponse.payload; if (payload != null) { debugPrint('notification payload: $payload'); - final collectionID = Uri.parse(payload).queryParameters["collectionID"]; - if (collectionID != null) { - final collection = CollectionsService.instance - .getCollectionByID(int.parse(collectionID))!; - final thumbnail = - await CollectionsService.instance.getCover(collection); + if (payload.toLowerCase().contains("onthisday")) { // ignore: unawaited_futures - routeToPage( - context, - CollectionPage( - CollectionWithThumbnail( - collection, - thumbnail, + memoriesCacheService.goToOnThisDayMemory(context); + } else { + final collectionID = Uri.parse(payload).queryParameters["collectionID"]; + if (collectionID != null) { + final collection = CollectionsService.instance + .getCollectionByID(int.parse(collectionID))!; + final thumbnail = + await CollectionsService.instance.getCover(collection); + // ignore: unawaited_futures + routeToPage( + context, + CollectionPage( + CollectionWithThumbnail( + collection, + thumbnail, + ), ), - ), - ); + ); + } } } } diff --git a/mobile/lib/ui/tabs/section_title.dart b/mobile/lib/ui/tabs/section_title.dart index 1f1be877f2..de5fbf146e 100644 --- a/mobile/lib/ui/tabs/section_title.dart +++ b/mobile/lib/ui/tabs/section_title.dart @@ -45,25 +45,31 @@ class SectionOptions extends StatelessWidget { final Widget title; final Widget? trailingWidget; final EdgeInsetsGeometry? padding; + final VoidCallback? onTap; const SectionOptions( this.title, { this.trailingWidget, this.padding = const EdgeInsets.only(left: 12, right: 0), super.key, + this.onTap, }); @override Widget build(BuildContext context) { if (trailingWidget != null) { - return Container( - padding: padding, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container(alignment: Alignment.centerLeft, child: title), - trailingWidget!, - ], + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: Container( + padding: padding, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Align(alignment: Alignment.centerLeft, child: title), + trailingWidget!, + ], + ), ), ); } else { diff --git a/mobile/lib/ui/tabs/shared/quick_link_album_item.dart b/mobile/lib/ui/tabs/shared/quick_link_album_item.dart index db84df9083..f5bf9fd6da 100644 --- a/mobile/lib/ui/tabs/shared/quick_link_album_item.dart +++ b/mobile/lib/ui/tabs/shared/quick_link_album_item.dart @@ -150,10 +150,11 @@ class QuickLinkAlbumItem extends StatelessWidget { iconButtonType: IconButtonType.secondary, iconColor: colorScheme.blurStrokeBase, ) - : const IconButtonWidget( - key: ValueKey("unselected"), + : IconButtonWidget( + key: const ValueKey("unselected"), icon: Icons.chevron_right_outlined, iconButtonType: IconButtonType.secondary, + iconColor: colorScheme.blurStrokePressed, ), ), ), diff --git a/mobile/lib/ui/tabs/shared_collections_tab.dart b/mobile/lib/ui/tabs/shared_collections_tab.dart index 1f0fc3d727..a3d9ec0e1e 100644 --- a/mobile/lib/ui/tabs/shared_collections_tab.dart +++ b/mobile/lib/ui/tabs/shared_collections_tab.dart @@ -3,13 +3,18 @@ import "dart:math"; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; +import "package:photos/core/constants.dart"; import 'package:photos/core/event_bus.dart'; import 'package:photos/events/collection_updated_event.dart'; import 'package:photos/events/local_photos_updated_event.dart'; +import "package:photos/events/tab_changed_event.dart"; import 'package:photos/events/user_logged_out_event.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/models/collection/collection_items.dart'; +import "package:photos/models/search/generic_search_result.dart"; import 'package:photos/services/collections_service.dart'; +import "package:photos/services/search_service.dart"; +import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/collections/album/row_item.dart"; import "package:photos/ui/collections/collection_list_page.dart"; import 'package:photos/ui/common/loading_widget.dart'; @@ -20,6 +25,7 @@ import "package:photos/ui/tabs/shared/empty_state.dart"; import "package:photos/ui/tabs/shared/quick_link_album_item.dart"; import "package:photos/ui/viewer/gallery/collect_photos_card_widget.dart"; import "package:photos/ui/viewer/gallery/collection_page.dart"; +import "package:photos/ui/viewer/search_tab/contacts_section.dart"; import "package:photos/utils/navigation_util.dart"; import "package:photos/utils/standalone/debouncer.dart"; @@ -43,6 +49,16 @@ class _SharedCollectionsTabState extends State leading: true, ); static const heroTagPrefix = "outgoing_collection"; + late StreamSubscription _tabChangeEvent; + + // This can be used to defer loading of widgets in this tab until the tab is + // selected for a certain amount of time. This will not turn true until the + // user has been in the tab for 500ms. This is to prevent loading widgets when + // the user is just switching tabs quickly. + final _canLoadDeferredWidgets = ValueNotifier(false); + final _debouncerForDeferringLoad = Debouncer( + const Duration(milliseconds: 500), + ); @override void initState() { @@ -68,6 +84,27 @@ class _SharedCollectionsTabState extends State _loggedOutEvent = Bus.instance.on().listen((event) { setState(() {}); }); + + _tabChangeEvent = Bus.instance.on().listen((event) { + if (event.selectedIndex == 2) { + _debouncerForDeferringLoad.run(() async { + _logger.info("Loading deferred widgets in shared collections tab"); + if (mounted) { + _canLoadDeferredWidgets.value = true; + await _tabChangeEvent.cancel(); + Future.delayed( + Duration.zero, + () => _debouncerForDeferringLoad.cancelDebounceTimer(), + ); + } + }); + } else { + _debouncerForDeferringLoad.cancelDebounceTimer(); + if (mounted) { + _canLoadDeferredWidgets.value = false; + } + } + }); } @override @@ -106,6 +143,7 @@ class _SharedCollectionsTabState extends State SectionTitle(title: S.of(context).sharedWithYou); final SectionTitle sharedByYou = SectionTitle(title: S.of(context).sharedByYou); + final colorTheme = getEnteColorScheme(context); return SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Container( @@ -118,25 +156,28 @@ class _SharedCollectionsTabState extends State mainAxisSize: MainAxisSize.min, children: [ SectionOptions( + onTap: collections.incoming.isNotEmpty + ? () { + unawaited( + routeToPage( + context, + CollectionListPage( + collections.incoming, + sectionType: + UISectionType.incomingCollections, + tag: "incoming", + appTitle: sharedWithYou, + ), + ), + ); + } + : null, Hero(tag: "incoming", child: sharedWithYou), trailingWidget: collections.incoming.isNotEmpty ? IconButtonWidget( icon: Icons.chevron_right, iconButtonType: IconButtonType.secondary, - onTap: () { - unawaited( - routeToPage( - context, - CollectionListPage( - collections.incoming, - sectionType: - UISectionType.incomingCollections, - tag: "incoming", - appTitle: sharedWithYou, - ), - ), - ); - }, + iconColor: colorTheme.blurStrokePressed, ) : null, ), @@ -156,6 +197,7 @@ class _SharedCollectionsTabState extends State collections.incoming[index], maxThumbnailWidth, tag: "incoming", + showFileCount: false, ), ); }, @@ -172,25 +214,28 @@ class _SharedCollectionsTabState extends State mainAxisSize: MainAxisSize.min, children: [ SectionOptions( + onTap: collections.outgoing.isNotEmpty + ? () { + unawaited( + routeToPage( + context, + CollectionListPage( + collections.outgoing, + sectionType: + UISectionType.outgoingCollections, + tag: "outgoing", + appTitle: sharedByYou, + ), + ), + ); + } + : null, Hero(tag: "outgoing", child: sharedByYou), trailingWidget: collections.outgoing.isNotEmpty ? IconButtonWidget( icon: Icons.chevron_right, iconButtonType: IconButtonType.secondary, - onTap: () { - unawaited( - routeToPage( - context, - CollectionListPage( - collections.outgoing, - sectionType: - UISectionType.outgoingCollections, - tag: "outgoing", - appTitle: sharedByYou, - ), - ), - ); - }, + iconColor: colorTheme.blurStrokePressed, ) : null, ), @@ -210,6 +255,7 @@ class _SharedCollectionsTabState extends State collections.outgoing[index], maxThumbnailWidth, tag: "outgoing", + showFileCount: false, ), ); }, @@ -228,6 +274,19 @@ class _SharedCollectionsTabState extends State child: Column( children: [ SectionOptions( + onTap: numberOfQuickLinks > maxQuickLinks + ? () { + unawaited( + routeToPage( + context, + AllQuickLinksPage( + titleHeroTag: quickLinkTitleHeroTag, + quickLinks: collections.quickLinks, + ), + ), + ); + } + : null, Hero( tag: quickLinkTitleHeroTag, child: SectionTitle( @@ -238,17 +297,7 @@ class _SharedCollectionsTabState extends State ? IconButtonWidget( icon: Icons.chevron_right, iconButtonType: IconButtonType.secondary, - onTap: () { - unawaited( - routeToPage( - context, - AllQuickLinksPage( - titleHeroTag: quickLinkTitleHeroTag, - quickLinks: collections.quickLinks, - ), - ), - ); - }, + iconColor: colorTheme.blurStrokePressed, ) : null, ), @@ -291,6 +340,34 @@ class _SharedCollectionsTabState extends State ), ) : const SizedBox.shrink(), + const SizedBox(height: 2), + ValueListenableBuilder( + valueListenable: _canLoadDeferredWidgets, + builder: (context, value, _) { + return value + ? FutureBuilder( + future: SearchService.instance + .getAllContactsSearchResults(kSearchSectionLimit), + builder: (context, snapshot) { + if (snapshot.hasData) { + return ContactsSection( + snapshot.data as List, + ); + } else if (snapshot.hasError) { + _logger.severe( + "failed to load contacts section", + snapshot.error, + snapshot.stackTrace, + ); + return const EnteLoadingWidget(); + } else { + return const EnteLoadingWidget(); + } + }, + ) + : const EnteLoadingWidget(); + }, + ), const SizedBox(height: 4), const CollectPhotosCardWidget(), const SizedBox(height: 32), @@ -306,6 +383,9 @@ class _SharedCollectionsTabState extends State _collectionUpdatesSubscription.cancel(); _loggedOutEvent.cancel(); _debouncer.cancelDebounceTimer(); + _debouncerForDeferringLoad.cancelDebounceTimer(); + _tabChangeEvent.cancel(); + _canLoadDeferredWidgets.dispose(); super.dispose(); } diff --git a/mobile/lib/ui/tabs/user_collections_tab.dart b/mobile/lib/ui/tabs/user_collections_tab.dart index afc6178c24..60de9b9afa 100644 --- a/mobile/lib/ui/tabs/user_collections_tab.dart +++ b/mobile/lib/ui/tabs/user_collections_tab.dart @@ -4,13 +4,14 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import "package:photos/core/configuration.dart"; import 'package:photos/core/event_bus.dart'; +import "package:photos/events/album_sort_order_change_event.dart"; import 'package:photos/events/collection_updated_event.dart'; import "package:photos/events/favorites_service_init_complete_event.dart"; import 'package:photos/events/local_photos_updated_event.dart'; import 'package:photos/events/user_logged_out_event.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/models/collection/collection.dart'; -import "package:photos/service_locator.dart"; +import "package:photos/models/selected_albums.dart"; import 'package:photos/services/collections_service.dart'; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/collections/button/archived_button.dart"; @@ -21,18 +22,19 @@ import "package:photos/ui/collections/collection_list_page.dart"; import "package:photos/ui/collections/device/device_folders_grid_view.dart"; import "package:photos/ui/collections/device/device_folders_vertical_grid_view.dart"; import "package:photos/ui/collections/flex_grid_view.dart"; -import "package:photos/ui/collections/new_album_icon.dart"; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import "package:photos/ui/tabs/section_title.dart"; +import "package:photos/ui/viewer/actions/album_selection_overlay_bar.dart"; import "package:photos/ui/viewer/actions/delete_empty_albums.dart"; import "package:photos/ui/viewer/gallery/empty_state.dart"; -import 'package:photos/utils/local_settings.dart'; import "package:photos/utils/navigation_util.dart"; import "package:photos/utils/standalone/debouncer.dart"; class UserCollectionsTab extends StatefulWidget { - const UserCollectionsTab({super.key}); + const UserCollectionsTab({super.key, this.selectedAlbums}); + + final SelectedAlbums? selectedAlbums; @override State createState() => _UserCollectionsTabState(); @@ -47,8 +49,8 @@ class _UserCollectionsTabState extends State late StreamSubscription _loggedOutEvent; late StreamSubscription _favoritesServiceInitCompleteEvent; + late StreamSubscription _albumSortOrderChangeEvent; - AlbumSortKey? sortKey; String _loadReason = "init"; final _scrollController = ScrollController(); final _debouncer = Debouncer( @@ -57,7 +59,7 @@ class _UserCollectionsTabState extends State leading: true, ); - static const int _kOnEnteItemLimitCount = 10; + static const int _kOnEnteItemLimitCount = 12; @override void initState() { super.initState(); @@ -90,7 +92,11 @@ class _UserCollectionsTabState extends State setState(() {}); }); }); - sortKey = localSettings.albumSortKey(); + _albumSortOrderChangeEvent = + Bus.instance.on().listen((event) { + _loadReason = event.reason; + setState(() {}); + }); } @override @@ -121,178 +127,113 @@ class _UserCollectionsTabState extends State .withOpacity(0.5), ); - return CustomScrollView( - physics: const BouncingScrollPhysics(), - controller: _scrollController, - slivers: [ - SliverToBoxAdapter( - child: SectionOptions( - Hero( - tag: "OnDeviceAppTitle", - child: SectionTitle(title: S.of(context).onDevice), - ), - trailingWidget: IconButtonWidget( - icon: Icons.chevron_right, - iconButtonType: IconButtonType.secondary, - onTap: () { - unawaited( - routeToPage( - context, - DeviceFolderVerticalGridView( - appTitle: SectionTitle( - title: S.of(context).onDevice, + return Stack( + alignment: Alignment.bottomCenter, + children: [ + CustomScrollView( + physics: const BouncingScrollPhysics(), + controller: _scrollController, + slivers: [ + SliverToBoxAdapter( + child: SectionOptions( + onTap: () { + unawaited( + routeToPage( + context, + DeviceFolderVerticalGridView( + appTitle: SectionTitle( + title: S.of(context).onDevice, + ), + tag: "OnDeviceAppTitle", ), - tag: "OnDeviceAppTitle", ), - ), - ); - }, + ); + }, + Hero( + tag: "OnDeviceAppTitle", + child: SectionTitle(title: S.of(context).onDevice), + ), + trailingWidget: IconButtonWidget( + icon: Icons.chevron_right, + iconButtonType: IconButtonType.secondary, + iconColor: getEnteColorScheme(context).blurStrokePressed, + ), + ), ), - ), - ), - const SliverToBoxAdapter(child: DeviceFoldersGridView()), - SliverToBoxAdapter( - child: SectionOptions( - SectionTitle(titleWithBrand: getOnEnteSection(context)), - trailingWidget: _sortMenu(collections), - padding: const EdgeInsets.only(left: 12, right: 6), - ), - ), - SliverToBoxAdapter(child: DeleteEmptyAlbums(collections)), - Configuration.instance.hasConfiguredAccount() - ? CollectionsFlexiGridViewWidget( - collections, - displayLimitCount: _kOnEnteItemLimitCount, - shrinkWrap: true, - ) - : const SliverToBoxAdapter(child: EmptyState()), - collections.length > _kOnEnteItemLimitCount - ? SliverToBoxAdapter( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - unawaited( - routeToPage( - context, - CollectionListPage( - collections, - sectionType: UISectionType.homeCollections, - appTitle: SectionTitle( - titleWithBrand: getOnEnteSection(context), - ), - initialScrollOffset: _scrollController.offset, + const SliverToBoxAdapter(child: DeviceFoldersGridView()), + SliverToBoxAdapter( + child: SectionOptions( + onTap: () { + unawaited( + routeToPage( + context, + CollectionListPage( + collections, + sectionType: UISectionType.homeCollections, + appTitle: SectionTitle( + titleWithBrand: getOnEnteSection(context), ), ), - ); - }, - child: SectionOptions( - SectionTitle( - title: S.of(context).viewAll, - mutedTitle: true, ), - trailingWidget: const IconButtonWidget( - icon: Icons.chevron_right, - iconButtonType: IconButtonType.secondary, - ), - ), + ); + }, + SectionTitle(titleWithBrand: getOnEnteSection(context)), + trailingWidget: IconButtonWidget( + icon: Icons.chevron_right, + iconButtonType: IconButtonType.secondary, + iconColor: getEnteColorScheme(context).blurStrokePressed, ), - ) - : const SliverToBoxAdapter(child: SizedBox.shrink()), - SliverToBoxAdapter( - child: Divider( - color: getEnteColorScheme(context).strokeFaint, - ), - ), - const SliverToBoxAdapter(child: SizedBox(height: 12)), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Column( - children: [ - UnCategorizedCollections(trashAndHiddenTextStyle), - const SizedBox(height: 12), - ArchivedCollectionsButton(trashAndHiddenTextStyle), - const SizedBox(height: 12), - HiddenCollectionsButtonWidget(trashAndHiddenTextStyle), - const SizedBox(height: 12), - TrashSectionButton(trashAndHiddenTextStyle), - ], + ), ), - ), + SliverToBoxAdapter(child: DeleteEmptyAlbums(collections)), + Configuration.instance.hasConfiguredAccount() + ? CollectionsFlexiGridViewWidget( + collections, + displayLimitCount: _kOnEnteItemLimitCount, + selectedAlbums: widget.selectedAlbums, + shrinkWrap: true, + shouldShowCreateAlbum: true, + enableSelectionMode: true, + ) + : const SliverToBoxAdapter(child: EmptyState()), + SliverToBoxAdapter( + child: Divider( + color: getEnteColorScheme(context).strokeFaint, + ), + ), + const SliverToBoxAdapter(child: SizedBox(height: 12)), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( + children: [ + UnCategorizedCollections(trashAndHiddenTextStyle), + const SizedBox(height: 12), + ArchivedCollectionsButton(trashAndHiddenTextStyle), + const SizedBox(height: 12), + HiddenCollectionsButtonWidget(trashAndHiddenTextStyle), + const SizedBox(height: 12), + TrashSectionButton(trashAndHiddenTextStyle), + ], + ), + ), + ), + SliverToBoxAdapter( + child: + SizedBox(height: 64 + MediaQuery.paddingOf(context).bottom), + ), + ], ), - SliverToBoxAdapter( - child: SizedBox(height: 64 + MediaQuery.of(context).padding.bottom), + AlbumSelectionOverlayBar( + widget.selectedAlbums!, + UISectionType.homeCollections, + collections, + showSelectAllButton: false, ), ], ); } - Widget _sortMenu(List collections) { - Text sortOptionText(AlbumSortKey key) { - String text = key.toString(); - switch (key) { - case AlbumSortKey.albumName: - text = S.of(context).name; - break; - case AlbumSortKey.newestPhoto: - text = S.of(context).newest; - break; - case AlbumSortKey.lastUpdated: - text = S.of(context).lastUpdated; - } - return Text( - text, - style: Theme.of(context).textTheme.titleMedium!.copyWith( - fontSize: 14, - color: Theme.of(context).iconTheme.color!.withOpacity(0.7), - ), - ); - } - - return Theme( - data: Theme.of(context).copyWith( - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - ), - child: Row( - children: [ - const NewAlbumIcon( - icon: Icons.add_rounded, - iconButtonType: IconButtonType.secondary, - ), - GestureDetector( - onTapDown: (TapDownDetails details) async { - final int? selectedValue = await showMenu( - context: context, - position: RelativeRect.fromLTRB( - details.globalPosition.dx, - details.globalPosition.dy, - details.globalPosition.dx, - details.globalPosition.dy + 50, - ), - items: List.generate(AlbumSortKey.values.length, (index) { - return PopupMenuItem( - value: index, - child: sortOptionText(AlbumSortKey.values[index]), - ); - }), - ); - if (selectedValue != null) { - sortKey = AlbumSortKey.values[selectedValue]; - await localSettings.setAlbumSortKey(sortKey!); - setState(() {}); - } - }, - child: const IconButtonWidget( - icon: Icons.sort_outlined, - iconButtonType: IconButtonType.secondary, - ), - ), - ], - ), - ); - } - @override void dispose() { _localFilesSubscription.cancel(); @@ -301,6 +242,7 @@ class _UserCollectionsTabState extends State _favoritesServiceInitCompleteEvent.cancel(); _scrollController.dispose(); _debouncer.cancelDebounceTimer(); + _albumSortOrderChangeEvent.cancel(); super.dispose(); } diff --git a/mobile/lib/ui/tools/editor/image_editor_page.dart b/mobile/lib/ui/tools/editor/image_editor_page.dart index 15f9e32041..99874d7c3a 100644 --- a/mobile/lib/ui/tools/editor/image_editor_page.dart +++ b/mobile/lib/ui/tools/editor/image_editor_page.dart @@ -429,11 +429,15 @@ class _ImageEditorPageState extends State { child: Row( children: [ SizedBox( - width: 40, - child: Text( - S.of(context).color, - style: subtitle2.copyWith( - color: subtitle2.color!.withOpacity(0.8), + width: 42, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + S.of(context).color, + style: subtitle2.copyWith( + color: subtitle2.color!.withOpacity(0.8), + ), ), ), ), @@ -475,11 +479,15 @@ class _ImageEditorPageState extends State { child: Row( children: [ SizedBox( - width: 40, - child: Text( - S.of(context).light, - style: subtitle2.copyWith( - color: subtitle2.color!.withOpacity(0.8), + width: 42, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + S.of(context).light, + style: subtitle2.copyWith( + color: subtitle2.color!.withOpacity(0.8), + ), ), ), ), diff --git a/mobile/lib/ui/viewer/actions/album_selection_action_widget.dart b/mobile/lib/ui/viewer/actions/album_selection_action_widget.dart new file mode 100644 index 0000000000..3dbc5af928 --- /dev/null +++ b/mobile/lib/ui/viewer/actions/album_selection_action_widget.dart @@ -0,0 +1,371 @@ +import "package:flutter/cupertino.dart"; +import "package:flutter/material.dart"; +import "package:logging/logging.dart"; +import "package:photos/db/files_db.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/models/collection/collection.dart"; +import "package:photos/models/metadata/common_keys.dart"; +import "package:photos/models/selected_albums.dart"; +import "package:photos/services/collections_service.dart"; +import "package:photos/ui/actions/collection/collection_sharing_actions.dart"; +import "package:photos/ui/collections/collection_list_page.dart"; +import "package:photos/ui/components/action_sheet_widget.dart"; +import "package:photos/ui/components/bottom_action_bar/selection_action_button_widget.dart"; +import "package:photos/ui/components/buttons/button_widget.dart"; +import "package:photos/ui/components/models/button_type.dart"; +import "package:photos/ui/notification/toast.dart"; +import "package:photos/ui/sharing/add_participant_page.dart"; +import "package:photos/utils/dialog_util.dart"; +import "package:photos/utils/magic_util.dart"; +import "package:photos/utils/navigation_util.dart"; + +class AlbumSelectionActionWidget extends StatefulWidget { + final SelectedAlbums selectedAlbums; + final UISectionType sectionType; + + const AlbumSelectionActionWidget( + this.selectedAlbums, + this.sectionType, { + super.key, + }); + + @override + State createState() => + _AlbumSelectionActionWidgetState(); +} + +class _AlbumSelectionActionWidgetState + extends State { + final _logger = Logger("AlbumSelectionActionWidgetState"); + late CollectionActions collectionActions; + bool hasFavorites = false; + + @override + initState() { + collectionActions = CollectionActions(CollectionsService.instance); + widget.selectedAlbums.addListener(_selectionChangedListener); + super.initState(); + } + + @override + void dispose() { + widget.selectedAlbums.removeListener(_selectionChangedListener); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.selectedAlbums.albums.isEmpty) { + return const SizedBox.shrink(); + } + final List items = []; + final hasPinnedAlbum = + widget.selectedAlbums.albums.any((album) => album.isPinned); + final hasUnpinnedAlbum = + widget.selectedAlbums.albums.any((album) => !album.isPinned); + + if (widget.sectionType == UISectionType.homeCollections || + widget.sectionType == UISectionType.outgoingCollections) { + items.add( + SelectionActionButton( + labelText: S.of(context).share, + icon: Icons.adaptive.share, + onTap: _shareCollection, + ), + ); + items.add( + SelectionActionButton( + labelText: "Pin", + icon: Icons.push_pin_rounded, + onTap: _onPinClick, + shouldShow: hasUnpinnedAlbum, + ), + ); + + items.add( + SelectionActionButton( + labelText: "Unpin", + icon: CupertinoIcons.pin_slash, + onTap: _onUnpinClick, + shouldShow: hasPinnedAlbum, + ), + ); + + items.add( + SelectionActionButton( + labelText: S.of(context).delete, + icon: Icons.delete_outline, + onTap: _trashCollection, + ), + ); + items.add( + SelectionActionButton( + labelText: S.of(context).hide, + icon: Icons.visibility_off_outlined, + onTap: _onHideClick, + ), + ); + } + + items.add( + SelectionActionButton( + labelText: S.of(context).archive, + icon: Icons.archive_outlined, + onTap: _archiveClick, + ), + ); + + if (widget.sectionType == UISectionType.incomingCollections) { + items.add( + SelectionActionButton( + labelText: S.of(context).leaveAlbum, + icon: Icons.logout, + onTap: _leaveAlbum, + ), + ); + } + + final scrollController = ScrollController(); + + return MediaQuery( + data: MediaQuery.of(context).removePadding(removeBottom: true), + child: SafeArea( + child: Scrollbar( + radius: const Radius.circular(1), + thickness: 2, + controller: scrollController, + thumbVisibility: true, + child: SingleChildScrollView( + physics: const BouncingScrollPhysics( + decelerationRate: ScrollDecelerationRate.fast, + ), + scrollDirection: Axis.horizontal, + child: Container( + padding: const EdgeInsets.only(bottom: 24), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(width: 4), + ...items, + const SizedBox(width: 4), + ], + ), + ), + ), + ), + ), + ); + } + + Future _shareCollection() async { + await routeToPage( + context, + AddParticipantPage( + widget.selectedAlbums.albums.toList(), + const [ActionTypesToShow.addViewer, ActionTypesToShow.addCollaborator], + ), + ); + widget.selectedAlbums.clearAll(); + } + + Future _trashCollection() async { + int count = 0; + final List nonEmptyCollection = []; + + final List errors = []; + for (final collection in widget.selectedAlbums.albums) { + if (collection.type == CollectionType.favorites) { + continue; + } + count = await FilesDB.instance.collectionFileCount(collection.id); + final bool isEmptyCollection = count == 0; + if (isEmptyCollection) { + try { + await CollectionsService.instance.trashEmptyCollection(collection); + } catch (e, s) { + _logger.warning("failed to trash collection", e, s); + errors.add(e); + } + } else { + nonEmptyCollection.add(collection); + } + } + if (errors.isNotEmpty) { + await showGenericErrorDialog( + context: context, + error: errors.first, + ); + } + + if (nonEmptyCollection.isNotEmpty) { + final bool result = await collectionActions.deleteMultipleCollectionSheet( + context, + nonEmptyCollection, + ); + if (result == false) { + debugPrint("Failed to delete collection"); + } + } + if (hasFavorites) { + _showFavToast(); + } + widget.selectedAlbums.clearAll(); + } + + Future _onPinClick() async { + for (final collection in widget.selectedAlbums.albums) { + if (collection.type == CollectionType.favorites || collection.isPinned) { + continue; + } + + await updateOrder( + context, + collection, + collection.isPinned ? 1 : 1, + ); + } + if (hasFavorites) { + _showFavToast(); + } + widget.selectedAlbums.clearAll(); + } + + Future _onUnpinClick() async { + for (final collection in widget.selectedAlbums.albums) { + if (collection.type == CollectionType.favorites || !collection.isPinned) { + continue; + } + + await updateOrder( + context, + collection, + collection.isPinned ? 0 : 0, + ); + } + if (hasFavorites) { + _showFavToast(); + } + widget.selectedAlbums.clearAll(); + } + + Future _onHideClick() async { + for (final collection in widget.selectedAlbums.albums) { + if (collection.type == CollectionType.favorites) { + continue; + } + final isHidden = collection.isHidden(); + final int prevVisiblity = isHidden ? hiddenVisibility : visibleVisibility; + final int newVisiblity = isHidden ? visibleVisibility : hiddenVisibility; + + await changeCollectionVisibility( + context, + collection: collection, + newVisibility: newVisiblity, + prevVisibility: prevVisiblity, + ); + } + if (hasFavorites) { + _showFavToast(); + } + widget.selectedAlbums.clearAll(); + } + + Future _archiveClick() async { + for (final collection in widget.selectedAlbums.albums) { + if (collection.type == CollectionType.favorites) { + continue; + } + if (widget.sectionType == UISectionType.incomingCollections) { + final hasShareeArchived = collection.hasShareeArchived(); + final int prevVisiblity = + hasShareeArchived ? archiveVisibility : visibleVisibility; + final int newVisiblity = + hasShareeArchived ? visibleVisibility : archiveVisibility; + + await changeCollectionVisibility( + context, + collection: collection, + newVisibility: newVisiblity, + prevVisibility: prevVisiblity, + isOwner: false, + ); + } else { + final isArchived = collection.isArchived(); + final int prevVisiblity = + isArchived ? archiveVisibility : visibleVisibility; + final int newVisiblity = + isArchived ? visibleVisibility : archiveVisibility; + + await changeCollectionVisibility( + context, + collection: collection, + newVisibility: newVisiblity, + prevVisibility: prevVisiblity, + ); + } + if (hasFavorites) { + _showFavToast(); + } + if (mounted) { + setState(() {}); + } + } + widget.selectedAlbums.clearAll(); + } + + Future _leaveAlbum() async { + final actionResult = await showActionSheet( + context: context, + buttons: [ + ButtonWidget( + buttonType: ButtonType.critical, + isInAlert: true, + shouldStickToDarkTheme: true, + buttonAction: ButtonAction.first, + shouldSurfaceExecutionStates: true, + labelText: S.of(context).leaveAlbum, + onTap: () async { + for (final collection in widget.selectedAlbums.albums) { + await CollectionsService.instance.leaveAlbum(collection); + } + widget.selectedAlbums.clearAll(); + }, + ), + ButtonWidget( + buttonType: ButtonType.secondary, + buttonAction: ButtonAction.cancel, + isInAlert: true, + shouldStickToDarkTheme: true, + labelText: S.of(context).cancel, + ), + ], + title: S.of(context).leaveSharedAlbum, + body: S.of(context).photosAddedByYouWillBeRemovedFromTheAlbum, + ); + if (actionResult?.action != null && mounted) { + if (actionResult!.action == ButtonAction.error) { + await showGenericErrorDialog( + context: context, + error: actionResult.exception, + ); + } else if (actionResult.action == ButtonAction.first) { + Navigator.of(context).pop(); + } + } + } + + void _selectionChangedListener() { + if (mounted) { + hasFavorites = widget.selectedAlbums.albums + .any((album) => album.type == CollectionType.favorites); + setState(() {}); + } + } + + void _showFavToast() { + showShortToast( + context, + S.of(context).actionNotSupportedOnFavouritesAlbum, + ); + } +} diff --git a/mobile/lib/ui/viewer/actions/album_selection_overlay_bar.dart b/mobile/lib/ui/viewer/actions/album_selection_overlay_bar.dart new file mode 100644 index 0000000000..1e0393bb03 --- /dev/null +++ b/mobile/lib/ui/viewer/actions/album_selection_overlay_bar.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import "package:photos/generated/l10n.dart"; +import "package:photos/models/collection/collection.dart"; +import "package:photos/models/selected_albums.dart"; +import "package:photos/theme/effects.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/collections/collection_list_page.dart"; +import "package:photos/ui/components/bottom_action_bar/album_bottom_action_bar_widget.dart"; + +class AlbumSelectionOverlayBar extends StatefulWidget { + final VoidCallback? onClose; + final SelectedAlbums selectedAlbums; + final List collections; + final Color? backgroundColor; + final UISectionType sectionType; + final bool showSelectAllButton; + + const AlbumSelectionOverlayBar( + this.selectedAlbums, + this.sectionType, + this.collections, { + super.key, + this.onClose, + this.backgroundColor, + this.showSelectAllButton = false, + }); + + @override + State createState() => + _AlbumSelectionOverlayBarState(); +} + +class _AlbumSelectionOverlayBarState extends State { + final ValueNotifier _hasSelectedAlbumsNotifier = ValueNotifier(false); + + @override + void initState() { + super.initState(); + widget.selectedAlbums.addListener(_selectedAlbumsListener); + } + + @override + void dispose() { + widget.selectedAlbums.removeListener(_selectedAlbumsListener); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: _hasSelectedAlbumsNotifier, + builder: (context, value, child) { + return AnimatedCrossFade( + firstCurve: Curves.easeInOutExpo, + secondCurve: Curves.easeInOutExpo, + sizeCurve: Curves.easeInOutExpo, + crossFadeState: _hasSelectedAlbumsNotifier.value + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 400), + firstChild: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (widget.showSelectAllButton) + Padding( + padding: const EdgeInsets.only(right: 4), + child: SelectAllAlbumsButton( + widget.selectedAlbums, + widget.collections, + backgroundColor: widget.backgroundColor, + ), + ), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration(boxShadow: shadowFloatFaintLight), + child: AlbumBottomActionBarWidget( + widget.selectedAlbums, + widget.sectionType, + onCancel: () { + if (widget.selectedAlbums.albums.isNotEmpty) { + widget.selectedAlbums.clearAll(); + } + }, + backgroundColor: widget.backgroundColor, + ), + ), + ], + ), + secondChild: const SizedBox(width: double.infinity), + ); + }, + ); + } + + _selectedAlbumsListener() { + _hasSelectedAlbumsNotifier.value = widget.selectedAlbums.albums.isNotEmpty; + } +} + +class SelectAllAlbumsButton extends StatefulWidget { + final SelectedAlbums selectedAlbums; + final List collections; + final Color? backgroundColor; + + const SelectAllAlbumsButton( + this.selectedAlbums, + this.collections, { + super.key, + this.backgroundColor, + }); + + @override + State createState() => _SelectAllAlbumsButtonState(); +} + +class _SelectAllAlbumsButtonState extends State { + bool _allSelected = false; + + @override + Widget build(BuildContext context) { + final colorScheme = getEnteColorScheme(context); + return GestureDetector( + onTap: () { + setState(() { + if (_allSelected) { + widget.selectedAlbums.clearAll(); + } else { + widget.selectedAlbums.select( + widget.collections.toSet(), + ); + } + _allSelected = !_allSelected; + }); + }, + child: Padding( + padding: const EdgeInsets.only(top: 8), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: widget.backgroundColor ?? colorScheme.backgroundElevated2, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, -1), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + S.of(context).selectAllShort, + style: getEnteTextTheme(context).miniMuted, + ), + const SizedBox(width: 4), + ListenableBuilder( + listenable: widget.selectedAlbums, + builder: (context, _) { + if (widget.selectedAlbums.albums.length == + widget.collections.length) { + _allSelected = true; + } else { + _allSelected = false; + } + return Icon( + _allSelected + ? Icons.check_circle + : Icons.check_circle_outline, + color: _allSelected ? null : colorScheme.strokeMuted, + size: 18, + ); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart b/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart index 3a3bcddcb5..9eb629cb4e 100644 --- a/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart +++ b/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart @@ -47,6 +47,7 @@ import "package:photos/utils/file_download_util.dart"; import 'package:photos/utils/magic_util.dart'; import 'package:photos/utils/navigation_util.dart'; import "package:photos/utils/share_util.dart"; +import "package:photos/utils/standalone/simple_task_queue.dart"; import "package:screenshot/screenshot.dart"; class FileSelectionActionsWidget extends StatefulWidget { @@ -886,12 +887,12 @@ class _FileSelectionActionsWidgetState ); await dialog.show(); try { - final downloadQueue = DownloadQueue(maxConcurrent: 5); + final taskQueue = SimpleTaskQueue(maxConcurrent: 5); final futures = []; for (final file in files) { if (file.localID == null) { futures.add( - downloadQueue.add(() async { + taskQueue.add(() async { await downloadToGallery(file); downloadedFiles++; dialog.update( diff --git a/mobile/lib/ui/viewer/date/edit_date_sheet.dart b/mobile/lib/ui/viewer/date/edit_date_sheet.dart index 1ae6a12eb2..39c5381909 100644 --- a/mobile/lib/ui/viewer/date/edit_date_sheet.dart +++ b/mobile/lib/ui/viewer/date/edit_date_sheet.dart @@ -264,7 +264,7 @@ class DateAndTimeWidget extends StatelessWidget { Widget build(BuildContext context) { final colorScheme = getEnteColorScheme(context); final locale = Localizations.localeOf(context); - final String date = DateFormat.yMMMd(locale.languageCode).format(dateTime); + final String date = DateFormat.yMMMd(locale.toString()).format(dateTime); final String time = DateFormat( MediaQuery.of(context).alwaysUse24HourFormat ? 'HH:mm' : 'h:mm a', ).format(dateTime); @@ -628,7 +628,7 @@ class PhotoDateHeaderWidget extends StatelessWidget { ), const SizedBox(height: 4), Text( - "${DateFormat.yMEd(locale.languageCode).format(startDate)} · ${DateFormat( + "${DateFormat.yMEd(locale.toString()).format(startDate)} · ${DateFormat( MediaQuery.of(context).alwaysUse24HourFormat ? 'HH:mm' : 'h:mm a', @@ -648,7 +648,7 @@ class PhotoDateHeaderWidget extends StatelessWidget { } String _formatDate(DateTime date, Locale locale, BuildContext context) { - return "${DateFormat.yMEd(locale.languageCode).format(date)}\n${DateFormat( + return "${DateFormat.yMEd(locale.toString()).format(date)}\n${DateFormat( MediaQuery.of(context).alwaysUse24HourFormat ? 'HH:mm' : 'h:mm a', ).format(date)}"; } diff --git a/mobile/lib/ui/viewer/file/file_details_widget.dart b/mobile/lib/ui/viewer/file/file_details_widget.dart index 12f722b357..58528159ae 100644 --- a/mobile/lib/ui/viewer/file/file_details_widget.dart +++ b/mobile/lib/ui/viewer/file/file_details_widget.dart @@ -18,7 +18,6 @@ import "package:photos/models/location/location.dart"; import "package:photos/models/metadata/file_magic.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/file_magic_service.dart"; -import "package:photos/services/filedata/filedata_service.dart"; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import "package:photos/ui/components/divider_widget.dart"; @@ -29,7 +28,7 @@ import "package:photos/ui/viewer/file_details/albums_item_widget.dart"; import 'package:photos/ui/viewer/file_details/backed_up_time_item_widget.dart'; import "package:photos/ui/viewer/file_details/creation_time_item_widget.dart"; import 'package:photos/ui/viewer/file_details/exif_item_widgets.dart'; -import "package:photos/ui/viewer/file_details/faces_item_widget.dart"; +import "package:photos/ui/viewer/file_details/file_info_faces_item_widget.dart"; import "package:photos/ui/viewer/file_details/file_properties_item_widget.dart"; import "package:photos/ui/viewer/file_details/location_tags_widget.dart"; import "package:photos/ui/viewer/file_details/preview_properties_item_widget.dart"; @@ -179,9 +178,8 @@ class _FileDetailsWidgetState extends State { ), const FileDetailsDivider(), if (widget.file.uploadedFileID != null && - (FileDataService.instance.previewIds - ?.containsKey(widget.file.uploadedFileID) ?? - false)) ...[ + (fileDataService.previewIds + .containsKey(widget.file.uploadedFileID))) ...[ ValueListenableBuilder( valueListenable: _exifNotifier, builder: (context, _, __) => PreviewPropertiesItemWidget( diff --git a/mobile/lib/ui/viewer/file/file_widget.dart b/mobile/lib/ui/viewer/file/file_widget.dart index a38f576c37..e33909dfcd 100644 --- a/mobile/lib/ui/viewer/file/file_widget.dart +++ b/mobile/lib/ui/viewer/file/file_widget.dart @@ -12,6 +12,8 @@ class FileWidget extends StatelessWidget { final Function(bool)? playbackCallback; final BoxDecoration? backgroundDecoration; final bool? autoPlay; + final bool? isFromMemories; + final Function(int)? onFinalFileLoad; const FileWidget( this.file, { @@ -20,6 +22,8 @@ class FileWidget extends StatelessWidget { this.playbackCallback, required this.tagPrefix, this.backgroundDecoration, + this.isFromMemories = false, + this.onFinalFileLoad, super.key, }); @@ -37,7 +41,9 @@ class FileWidget extends StatelessWidget { shouldDisableScroll: shouldDisableScroll, tagPrefix: tagPrefix, backgroundDecoration: backgroundDecoration, + isFromMemories: isFromMemories ?? false, key: key ?? ValueKey(fileKey), + onFinalFileLoad: onFinalFileLoad, ); } else if (file.fileType == FileType.video) { // use old video widget on iOS simulator as the new one crashes while @@ -54,6 +60,8 @@ class FileWidget extends StatelessWidget { file, tagPrefix: tagPrefix, playbackCallback: playbackCallback, + onFinalFileLoad: onFinalFileLoad, + isFromMemories: isFromMemories ?? false, key: key ?? ValueKey(fileKey), ); } else { diff --git a/mobile/lib/ui/viewer/file/native_video_player_controls/seek_bar.dart b/mobile/lib/ui/viewer/file/native_video_player_controls/seek_bar.dart index 0348dae0e9..7a1be9dc25 100644 --- a/mobile/lib/ui/viewer/file/native_video_player_controls/seek_bar.dart +++ b/mobile/lib/ui/viewer/file/native_video_player_controls/seek_bar.dart @@ -5,7 +5,6 @@ import "package:native_video_player/native_video_player.dart"; import "package:photos/core/event_bus.dart"; import "package:photos/events/seekbar_triggered_event.dart"; import "package:photos/theme/colors.dart"; -import "package:photos/theme/ente_theme.dart"; import "package:photos/utils/standalone/debouncer.dart"; class SeekBar extends StatefulWidget { @@ -64,7 +63,6 @@ class _SeekBarState extends State with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { - final colorScheme = getEnteColorScheme(context); return AnimatedBuilder( animation: _animationController, builder: (_, __) { @@ -74,7 +72,7 @@ class _SeekBarState extends State with SingleTickerProviderStateMixin { tickMarkShape: SliderTickMarkShape.noTickMark, thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8.0), overlayShape: const RoundSliderOverlayShape(overlayRadius: 14.0), - activeTrackColor: colorScheme.primary300, + activeTrackColor: backgroundElevatedLight, inactiveTrackColor: fillMutedDark, thumbColor: backgroundElevatedLight, overlayColor: fillMutedDark, diff --git a/mobile/lib/ui/viewer/file/preview_status_widget.dart b/mobile/lib/ui/viewer/file/preview_status_widget.dart deleted file mode 100644 index 1e1d2e2c98..0000000000 --- a/mobile/lib/ui/viewer/file/preview_status_widget.dart +++ /dev/null @@ -1,184 +0,0 @@ -import "dart:async"; - -import "package:flutter/material.dart"; -import "package:photos/core/event_bus.dart"; -import "package:photos/events/preview_updated_event.dart"; -import "package:photos/generated/l10n.dart"; -import "package:photos/models/file/file.dart"; -import "package:photos/models/preview/preview_item.dart"; -import "package:photos/models/preview/preview_item_status.dart"; -import "package:photos/services/filedata/filedata_service.dart"; -import "package:photos/services/preview_video_store.dart"; -import "package:photos/theme/colors.dart"; - -class PreviewStatusWidget extends StatefulWidget { - const PreviewStatusWidget({ - super.key, - required bool showControls, - required this.file, - required this.onStreamChange, - this.isPreviewPlayer = false, - }) : _showControls = showControls; - - final bool _showControls; - final EnteFile file; - final bool isPreviewPlayer; - final void Function()? onStreamChange; - - @override - State createState() => _PreviewStatusWidgetState(); -} - -class _PreviewStatusWidgetState extends State { - StreamSubscription? previewSubscription; - late PreviewItem? preview = - PreviewVideoStore.instance.previews[widget.file.uploadedFileID]; - late bool isVideoStreamingEnabled; - - @override - void initState() { - super.initState(); - - isVideoStreamingEnabled = - PreviewVideoStore.instance.isVideoStreamingEnabled; - if (!isVideoStreamingEnabled) { - return; - } - previewSubscription = - Bus.instance.on().listen((event) { - final newPreview = event.items[widget.file.uploadedFileID]; - if (newPreview != preview) { - setState(() { - preview = event.items[widget.file.uploadedFileID]; - }); - } - }); - } - - @override - void dispose() { - previewSubscription?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (!isVideoStreamingEnabled) { - return const SizedBox(); - } - final bool isPreviewAvailable = widget.file.uploadedFileID != null && - (FileDataService.instance.previewIds - ?.containsKey(widget.file.uploadedFileID) ?? - false); - - if (preview == null && !isPreviewAvailable) { - return const SizedBox(); - } - final isInProgress = preview?.status == PreviewItemStatus.compressing || - preview?.status == PreviewItemStatus.uploading; - final isInQueue = preview?.status == PreviewItemStatus.inQueue || - preview?.status == PreviewItemStatus.retry; - final isFailed = preview?.status == PreviewItemStatus.failed; - - final isBeforeCutoffDate = widget.file.creationTime != null && - PreviewVideoStore.instance.videoStreamingCutoff != null - ? DateTime.fromMillisecondsSinceEpoch(widget.file.creationTime!) - .isBefore( - PreviewVideoStore.instance.videoStreamingCutoff!, - ) - : false; - - if (preview == null && isBeforeCutoffDate && !isPreviewAvailable) { - return const SizedBox(); - } - - return Align( - alignment: Alignment.centerRight, - child: AnimatedOpacity( - duration: const Duration( - milliseconds: 200, - ), - curve: Curves.easeInQuad, - opacity: widget._showControls ? 1 : 0, - child: Padding( - padding: const EdgeInsets.only( - right: 10, - bottom: 4, - ), - child: GestureDetector( - onTap: - preview == null || preview!.status == PreviewItemStatus.uploaded - ? widget.onStreamChange - : null, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: isPreviewAvailable ? Colors.green : null, - borderRadius: const BorderRadius.all( - Radius.circular(200), - ), - border: isPreviewAvailable - ? null - : Border.all( - color: strokeFaintDark, - width: 1, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - !isInProgress - ? Icon( - isInQueue - ? Icons.history_outlined - : isBeforeCutoffDate - ? Icons.block_outlined - : isFailed - ? Icons.error_outline - : Icons.play_arrow, - size: 16, - ) - : const SizedBox( - width: 12, - height: 12, - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - Colors.white, - ), - backgroundColor: Colors.transparent, - strokeWidth: 2, - ), - ), - SizedBox( - width: !isInProgress || isPreviewAvailable ? 2 : 6, - ), - Text( - isInProgress - ? S.of(context).processing - : isInQueue - ? S.of(context).queued - : isBeforeCutoffDate - ? S.of(context).ineligible - : isFailed - ? S.of(context).failed - : widget.isPreviewPlayer - ? S.of(context).playOriginal - : S.of(context).playStream, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/ui/viewer/file/video_stream_change.dart b/mobile/lib/ui/viewer/file/video_stream_change.dart new file mode 100644 index 0000000000..af9a14f1d3 --- /dev/null +++ b/mobile/lib/ui/viewer/file/video_stream_change.dart @@ -0,0 +1,97 @@ +import "package:flutter/material.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/models/file/file.dart"; +import "package:photos/service_locator.dart"; +import "package:photos/theme/colors.dart"; + +class VideoStreamChangeWidget extends StatefulWidget { + const VideoStreamChangeWidget({ + super.key, + required bool showControls, + required this.file, + required this.onStreamChange, + this.isPreviewPlayer = false, + }) : _showControls = showControls; + + final bool _showControls; + final EnteFile file; + final bool isPreviewPlayer; + final void Function()? onStreamChange; + + @override + State createState() => + _VideoStreamChangeWidgetState(); +} + +class _VideoStreamChangeWidgetState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final bool isPreviewAvailable = widget.file.uploadedFileID != null && + (fileDataService.previewIds.containsKey(widget.file.uploadedFileID)); + if (!isPreviewAvailable) { + return const SizedBox(); + } + return Align( + alignment: Alignment.centerRight, + child: AnimatedOpacity( + duration: const Duration( + milliseconds: 200, + ), + curve: Curves.easeInQuad, + opacity: widget._showControls ? 1 : 0, + child: Padding( + padding: const EdgeInsets.only( + right: 10, + bottom: 4, + ), + child: GestureDetector( + onTap: widget.onStreamChange, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.green, + borderRadius: const BorderRadius.all( + Radius.circular(200), + ), + border: Border.all( + color: strokeFaintDark, + width: 1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.play_arrow, size: 16), + const SizedBox(width: 2), + Text( + widget.isPreviewPlayer + ? S.of(context).playOriginal + : S.of(context).playStream, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/mobile/lib/ui/viewer/file/video_widget.dart b/mobile/lib/ui/viewer/file/video_widget.dart index da08aa1c31..96816fd392 100644 --- a/mobile/lib/ui/viewer/file/video_widget.dart +++ b/mobile/lib/ui/viewer/file/video_widget.dart @@ -11,7 +11,6 @@ import "package:photos/models/file/extensions/file_props.dart"; import "package:photos/models/file/file.dart"; import "package:photos/models/preview/playlist_data.dart"; import "package:photos/service_locator.dart"; -import "package:photos/services/filedata/filedata_service.dart"; import "package:photos/services/preview_video_store.dart"; import "package:photos/theme/colors.dart"; import "package:photos/ui/common/loading_widget.dart"; @@ -24,10 +23,15 @@ class VideoWidget extends StatefulWidget { final EnteFile file; final String? tagPrefix; final Function(bool)? playbackCallback; + final Function(int)? onFinalFileLoad; + final bool isFromMemories; + const VideoWidget( this.file, { this.tagPrefix, this.playbackCallback, + this.onFinalFileLoad, + this.isFromMemories = false, super.key, }); @@ -58,13 +62,11 @@ class _VideoWidgetState extends State { }); }); if (widget.file.isUploaded) { - isPreviewLoadable = FileDataService.instance.previewIds - ?.containsKey(widget.file.uploadedFileID) ?? - false; - if (!widget.file.isOwner && flagService.internalUser) { - // todo: neeraj assume shared files are previewable, fetch the preview data - // and mark as not previewable if not available. Add backend support for - // fetching preview status to cache this information proactively + isPreviewLoadable = + fileDataService.previewIds.containsKey(widget.file.uploadedFileID); + if (!widget.file.isOwner) { + // For shared video, we need to on-demand check if the file is streamable + // and if not, we need to set isPreviewLoadable to false isPreviewLoadable = true; } _checkForPreview(); @@ -78,7 +80,7 @@ class _VideoWidgetState extends State { } Future _checkForPreview() async { - if (!widget.file.isOwner && flagService.internalUser) { + if (!widget.file.isOwner) { final bool isStreamable = await PreviewVideoStore.instance.isSharedFileStreamble(widget.file); if (!isStreamable && mounted) { @@ -152,6 +154,7 @@ class _VideoWidgetState extends State { playbackCallback: widget.playbackCallback, playlistData: playlistData, selectedPreview: playPreview, + isFromMemories: widget.isFromMemories, onStreamChange: () { setState(() { selectPreviewForPlay = !selectPreviewForPlay; @@ -165,6 +168,7 @@ class _VideoWidgetState extends State { ); }); }, + onFinalFileLoad: widget.onFinalFileLoad, ); } return VideoWidgetMediaKitNew( @@ -174,6 +178,7 @@ class _VideoWidgetState extends State { playbackCallback: widget.playbackCallback, preview: playlistData?.preview, selectedPreview: playPreview, + isFromMemories: widget.isFromMemories, onStreamChange: () { setState(() { selectPreviewForPlay = !selectPreviewForPlay; @@ -187,6 +192,7 @@ class _VideoWidgetState extends State { ); }); }, + onFinalFileLoad: widget.onFinalFileLoad, ); } } diff --git a/mobile/lib/ui/viewer/file/video_widget_media_kit.dart b/mobile/lib/ui/viewer/file/video_widget_media_kit.dart index ae1f86dd6b..50beceb5f8 100644 --- a/mobile/lib/ui/viewer/file/video_widget_media_kit.dart +++ b/mobile/lib/ui/viewer/file/video_widget_media_kit.dart @@ -124,7 +124,6 @@ class _VideoWidgetMediaKitState extends State @override Widget build(BuildContext context) { - final colorScheme = getEnteColorScheme(context); return Hero( tag: widget.tagPrefix! + widget.file.tag, child: MaterialVideoControlsTheme( @@ -142,7 +141,7 @@ class _VideoWidgetMediaKitState extends State seekBarBufferColor: Colors.transparent, seekBarThumbColor: backgroundElevatedLight, seekBarColor: fillMutedDark, - seekBarPositionColor: colorScheme.primary300, + seekBarPositionColor: backgroundElevatedLight, seekBarContainerHeight: 56, seekBarAlignment: Alignment.center, diff --git a/mobile/lib/ui/viewer/file/video_widget_media_kit_common.dart b/mobile/lib/ui/viewer/file/video_widget_media_kit_common.dart index 69e3f5eca9..34234e2259 100644 --- a/mobile/lib/ui/viewer/file/video_widget_media_kit_common.dart +++ b/mobile/lib/ui/viewer/file/video_widget_media_kit_common.dart @@ -7,7 +7,7 @@ import "package:photos/theme/colors.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/actions/file/file_actions.dart"; import "package:photos/ui/common/loading_widget.dart"; -import "package:photos/ui/viewer/file/preview_status_widget.dart"; +import "package:photos/ui/viewer/file/video_stream_change.dart"; import "package:photos/utils/standalone/date_time.dart"; import "package:photos/utils/standalone/debouncer.dart"; @@ -100,6 +100,7 @@ class _VideoWidgetState extends State { GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { + if (widget.isFromMemories) return; showControlsNotifier.value = !showControlsNotifier.value; if (widget.playbackCallback != null) { widget.playbackCallback!( @@ -107,48 +108,62 @@ class _VideoWidgetState extends State { ); } }, + onLongPress: () { + if (widget.isFromMemories) { + widget.controller.player.stop(); + } + }, + onLongPressUp: () { + if (widget.isFromMemories) { + widget.controller.player.play(); + } + }, child: Container( constraints: const BoxConstraints.expand(), ), ), - IgnorePointer( - ignoring: !value, - child: PlayPauseButtonMediaKit(widget.controller), - ), - Positioned( - bottom: verticalMargin, - right: 0, - left: 0, - child: IgnorePointer( - ignoring: !value, - child: SafeArea( - top: false, - left: false, - right: false, - child: Padding( - padding: EdgeInsets.only( - bottom: widget.isFromMemories ? 32 : 0, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - PreviewStatusWidget( - showControls: value, - file: widget.file, - isPreviewPlayer: widget.isPreviewPlayer, - onStreamChange: widget.onStreamChange, + widget.isFromMemories + ? const SizedBox.shrink() + : IgnorePointer( + ignoring: !value, + child: PlayPauseButtonMediaKit(widget.controller), + ), + widget.isFromMemories + ? const SizedBox.shrink() + : Positioned( + bottom: verticalMargin, + right: 0, + left: 0, + child: IgnorePointer( + ignoring: !value, + child: SafeArea( + top: false, + left: false, + right: false, + child: Padding( + padding: EdgeInsets.only( + bottom: widget.isFromMemories ? 32 : 0, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + VideoStreamChangeWidget( + showControls: value, + file: widget.file, + isPreviewPlayer: widget.isPreviewPlayer, + onStreamChange: widget.onStreamChange, + ), + SeekBarAndDuration( + controller: widget.controller, + isSeekingNotifier: _isSeekingNotifier, + file: widget.file, + ), + ], + ), ), - SeekBarAndDuration( - controller: widget.controller, - isSeekingNotifier: _isSeekingNotifier, - file: widget.file, - ), - ], + ), ), ), - ), - ), - ), ], ), ); @@ -420,13 +435,12 @@ class _SeekBarState extends State { @override Widget build(BuildContext context) { - final colorScheme = getEnteColorScheme(context); return SliderTheme( data: SliderTheme.of(context).copyWith( trackHeight: 1.0, thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8.0), overlayShape: const RoundSliderOverlayShape(overlayRadius: 14.0), - activeTrackColor: colorScheme.primary300, + activeTrackColor: backgroundElevatedLight, inactiveTrackColor: fillMutedDark, thumbColor: backgroundElevatedLight, overlayColor: fillMutedDark, diff --git a/mobile/lib/ui/viewer/file/video_widget_media_kit_new.dart b/mobile/lib/ui/viewer/file/video_widget_media_kit_new.dart index 97fb663577..a362050759 100644 --- a/mobile/lib/ui/viewer/file/video_widget_media_kit_new.dart +++ b/mobile/lib/ui/viewer/file/video_widget_media_kit_new.dart @@ -14,10 +14,12 @@ import "package:photos/events/stream_switched_event.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/models/file/extensions/file_props.dart"; import "package:photos/models/file/file.dart"; +import "package:photos/module/download/task.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/files_service.dart"; import "package:photos/services/wake_lock_service.dart"; import "package:photos/theme/colors.dart"; +import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/actions/file/file_actions.dart"; import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/notification/toast.dart"; @@ -34,6 +36,7 @@ class VideoWidgetMediaKitNew extends StatefulWidget { final void Function() onStreamChange; final File? preview; final bool selectedPreview; + final Function(int)? onFinalFileLoad; const VideoWidgetMediaKitNew( this.file, { @@ -43,6 +46,7 @@ class VideoWidgetMediaKitNew extends StatefulWidget { required this.onStreamChange, this.preview, required this.selectedPreview, + this.onFinalFileLoad, super.key, }); @@ -62,6 +66,7 @@ class _VideoWidgetMediaKitNewState extends State late final StreamSubscription _guestViewEventSubscription; bool _isGuestView = false; StreamSubscription? _streamSwitchedSubscription; + StreamSubscription? _downloadTaskSubscription; late final StreamSubscription _captionUpdatedSubscription; @@ -88,6 +93,17 @@ class _VideoWidgetMediaKitNewState extends State _isGuestView = event.isGuestView; }); }); + if (widget.file.isUploaded) { + _downloadTaskSubscription = downloadManager + .watchDownload(widget.file.uploadedFileID!) + .listen((event) { + if (mounted) { + setState(() { + _progressNotifier.value = event.progress; + }); + } + }); + } _streamSwitchedSubscription = Bus.instance.on().listen((event) { @@ -160,6 +176,10 @@ class _VideoWidgetMediaKitNewState extends State removeCallBack(widget.file); _progressNotifier.dispose(); WidgetsBinding.instance.removeObserver(this); + if (_downloadTaskSubscription != null) { + _downloadTaskSubscription!.cancel(); + downloadManager.pause(widget.file.uploadedFileID!).ignore(); + } player.dispose(); _captionUpdatedSubscription.cancel(); if (EnteWakeLockService.instance.shouldKeepAppAwakeAcrossSessions) { @@ -196,11 +216,40 @@ class _VideoWidgetMediaKitNewState extends State onStreamChange: widget.onStreamChange, isPreviewPlayer: widget.selectedPreview, ) - : const Center( - child: EnteLoadingWidget( - size: 32, - color: fillBaseDark, - padding: 0, + : Center( + child: ValueListenableBuilder( + valueListenable: _progressNotifier, + builder: (BuildContext context, double? progress, _) { + return progress == null || progress == 1 + ? const EnteLoadingWidget( + size: 32, + color: fillBaseDark, + padding: 0, + ) + : Stack( + children: [ + CircularProgressIndicator( + backgroundColor: Colors.transparent, + value: progress, + valueColor: const AlwaysStoppedAnimation( + Color.fromRGBO(45, 194, 98, 1.0), + ), + strokeWidth: 2, + strokeCap: StrokeCap.round, + ), + if (flagService.internalUser) + Center( + child: Text( + "${(progress * 100).toStringAsFixed(0)}%", + style: + getEnteTextTheme(context).tiny.copyWith( + color: textBaseDark, + ), + ), + ), + ], + ); + }, ), ), ), @@ -260,6 +309,8 @@ class _VideoWidgetMediaKitNewState extends State } player.open(Media(url), play: _isAppInFG); }); + final duration = controller!.player.state.duration.inSeconds; + widget.onFinalFileLoad?.call(duration); } } } diff --git a/mobile/lib/ui/viewer/file/video_widget_native.dart b/mobile/lib/ui/viewer/file/video_widget_native.dart index f4b63c2ed4..f375d95673 100644 --- a/mobile/lib/ui/viewer/file/video_widget_native.dart +++ b/mobile/lib/ui/viewer/file/video_widget_native.dart @@ -18,6 +18,7 @@ import "package:photos/generated/l10n.dart"; import "package:photos/models/file/extensions/file_props.dart"; import "package:photos/models/file/file.dart"; import "package:photos/models/preview/playlist_data.dart"; +import "package:photos/module/download/task.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/files_service.dart"; import "package:photos/services/wake_lock_service.dart"; @@ -28,8 +29,8 @@ import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/notification/toast.dart"; import "package:photos/ui/viewer/file/native_video_player_controls/play_pause_button.dart"; import "package:photos/ui/viewer/file/native_video_player_controls/seek_bar.dart"; -import "package:photos/ui/viewer/file/preview_status_widget.dart"; import "package:photos/ui/viewer/file/thumbnail_widget.dart"; +import "package:photos/ui/viewer/file/video_stream_change.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/exif_util.dart"; import "package:photos/utils/file_util.dart"; @@ -45,6 +46,7 @@ class VideoWidgetNative extends StatefulWidget { final void Function()? onStreamChange; final PlaylistData? playlistData; final bool selectedPreview; + final Function(int)? onFinalFileLoad; const VideoWidgetNative( this.file, { @@ -54,6 +56,7 @@ class VideoWidgetNative extends StatefulWidget { required this.onStreamChange, super.key, this.playlistData, + this.onFinalFileLoad, required this.selectedPreview, }); @@ -82,6 +85,7 @@ class _VideoWidgetNativeState extends State final _elTooltipController = ElTooltipController(); StreamSubscription? _subscription; StreamSubscription? _streamSwitchedSubscription; + StreamSubscription? downloadTaskSubscription; late final StreamSubscription _captionUpdatedSubscription; int position = 0; @@ -112,6 +116,7 @@ class _VideoWidgetNativeState extends State _streamSwitchedSubscription = Bus.instance.on().listen((event) { if (event.type != PlayerType.nativeVideoPlayer) return; + _filePath = null; if (event.selectedPreview) { loadPreview(update: true); } else { @@ -127,12 +132,30 @@ class _VideoWidgetNativeState extends State } } }); + if (widget.file.isUploaded) { + downloadTaskSubscription = downloadManager + .watchDownload( + widget.file.uploadedFileID!, + ) + .listen((event) { + if (mounted) { + setState(() { + _progressNotifier.value = event.progress; + }); + } + }); + } EnteWakeLockService.instance .updateWakeLock(enable: true, wakeLockFor: WakeLockFor.videoPlayback); } Future setVideoSource() async { + if (_filePath == null) { + _logger.info('Stop video player, file path is null'); + await _controller?.stop(); + return; + } final videoSource = VideoSource( path: _filePath!, type: VideoSourceType.file, @@ -195,6 +218,10 @@ class _VideoWidgetNativeState extends State void dispose() { _subscription?.cancel(); _controller?.dispose(); + if (downloadTaskSubscription != null) { + downloadTaskSubscription!.cancel(); + downloadManager.pause(widget.file.uploadedFileID!).ignore(); + } //https://github.com/fluttercandies/flutter_photo_manager/blob/8afba2745ebaac6af8af75de9cbded9157bc2690/README.md#clear-caches if (_shouldClearCache) { @@ -277,12 +304,23 @@ class _VideoWidgetNativeState extends State GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { + if (widget.isFromMemories) return; _showControls.value = !_showControls.value; if (widget.playbackCallback != null) { widget.playbackCallback!(!_showControls.value); } _elTooltipController.hide(); }, + onLongPress: () { + if (widget.isFromMemories) { + _controller?.pause(); + } + }, + onLongPressUp: () { + if (widget.isFromMemories) { + _controller?.play(); + } + }, child: Container( constraints: const BoxConstraints.expand(), ), @@ -306,32 +344,38 @@ class _VideoWidgetNativeState extends State ), ) : const SizedBox.shrink(), - Positioned.fill( - child: Center( - child: ValueListenableBuilder( - builder: (BuildContext context, bool value, _) { - return value - ? ValueListenableBuilder( - builder: (context, bool value, _) { - return AnimatedOpacity( - duration: - const Duration(milliseconds: 200), - opacity: value ? 1 : 0, - curve: Curves.easeInOutQuad, - child: IgnorePointer( - ignoring: !value, - child: PlayPauseButton(_controller), - ), - ); - }, - valueListenable: _showControls, - ) - : const SizedBox(); - }, - valueListenable: _isPlaybackReady, - ), - ), - ), + widget.isFromMemories + ? const SizedBox.shrink() + : Positioned.fill( + child: Center( + child: ValueListenableBuilder( + builder: + (BuildContext context, bool value, _) { + return value + ? ValueListenableBuilder( + builder: (context, bool value, _) { + return AnimatedOpacity( + duration: const Duration( + milliseconds: 200, + ), + opacity: value ? 1 : 0, + curve: Curves.easeInOutQuad, + child: IgnorePointer( + ignoring: !value, + child: PlayPauseButton( + _controller, + ), + ), + ); + }, + valueListenable: _showControls, + ) + : const SizedBox(); + }, + valueListenable: _isPlaybackReady, + ), + ), + ), Positioned( bottom: verticalMargin, right: 0, @@ -357,7 +401,7 @@ class _VideoWidgetNativeState extends State ValueListenableBuilder( valueListenable: _showControls, builder: (context, value, _) { - return PreviewStatusWidget( + return VideoStreamChangeWidget( showControls: value, file: widget.file, isPreviewPlayer: widget.selectedPreview, @@ -369,7 +413,7 @@ class _VideoWidgetNativeState extends State valueListenable: _isPlaybackReady, builder: (BuildContext context, bool value, _) { - return value + return value && !widget.isFromMemories ? _SeekBarAndDuration( controller: _controller, duration: duration, @@ -500,6 +544,8 @@ class _VideoWidgetNativeState extends State Future _onPlaybackReady() async { if (_isPlaybackReady.value) return; await _controller!.play(); + final durationInSeconds = durationToSeconds(duration) ?? 0; + widget.onFinalFileLoad?.call(durationInSeconds); unawaited(_controller!.setVolume(1)); _isPlaybackReady.value = true; } @@ -598,14 +644,27 @@ class _VideoWidgetNativeState extends State color: fillBaseDark, padding: 0, ) - : CircularProgressIndicator( - backgroundColor: Colors.transparent, - value: progress, - valueColor: const AlwaysStoppedAnimation( - Color.fromRGBO(45, 194, 98, 1.0), - ), - strokeWidth: 2, - strokeCap: StrokeCap.round, + : Stack( + children: [ + CircularProgressIndicator( + backgroundColor: Colors.transparent, + value: progress, + valueColor: const AlwaysStoppedAnimation( + Color.fromRGBO(45, 194, 98, 1.0), + ), + strokeWidth: 2, + strokeCap: StrokeCap.round, + ), + if (flagService.internalUser) + Center( + child: Text( + "${(progress * 100).toStringAsFixed(0)}%", + style: getEnteTextTheme(context).tiny.copyWith( + color: textBaseDark, + ), + ), + ), + ], ); }, ), @@ -646,9 +705,14 @@ class _VideoWidgetNativeState extends State if (widget.playlistData != null && widget.selectedPreview) { aspectRatio = widget.playlistData!.width! / widget.playlistData!.height!; - if (widget.file.duration != null && - (duration == "0:00" || duration == null)) { - duration = secondsToDuration(widget.file.duration!); + if (duration == "0:00" || duration == null) { + if ((widget.file.duration ?? 0) > 0) { + duration = secondsToDuration(widget.file.duration!); + } else if (widget.playlistData!.durationInSeconds != null) { + duration = secondsToDuration( + widget.playlistData!.durationInSeconds!, + ); + } } _logger.info("Getting aspect ratio from preview video"); return; @@ -696,11 +760,7 @@ class _SeekBarAndDuration extends StatelessWidget { Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: showControls, - builder: ( - BuildContext context, - bool value, - _, - ) { + builder: (BuildContext context, bool value, _) { return AnimatedOpacity( duration: const Duration( milliseconds: 200, diff --git a/mobile/lib/ui/viewer/file/zoomable_image.dart b/mobile/lib/ui/viewer/file/zoomable_image.dart index f9f2379369..7d78f5268d 100644 --- a/mobile/lib/ui/viewer/file/zoomable_image.dart +++ b/mobile/lib/ui/viewer/file/zoomable_image.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:typed_data' show Uint8List; import 'package:flutter/material.dart'; import "package:flutter_image_compress/flutter_image_compress.dart"; @@ -30,6 +31,7 @@ class ZoomableImage extends StatefulWidget { final Decoration? backgroundDecoration; final bool shouldCover; final bool isGuestView; + final Function(int)? onFinalFileLoad; const ZoomableImage( this.photo, { @@ -39,6 +41,7 @@ class ZoomableImage extends StatefulWidget { this.backgroundDecoration, this.shouldCover = false, this.isGuestView = false, + this.onFinalFileLoad, }); @override @@ -62,6 +65,10 @@ class _ZoomableImageState extends State { late final StreamSubscription _captionUpdatedSubscription; + // This is to prevent the app from crashing when loading 200MP images + // https://github.com/flutter/flutter/issues/110331 + bool get isTooLargeImage => _photo.width * _photo.height > 160000000; + @override void initState() { super.initState(); @@ -362,10 +369,33 @@ class _ZoomableImageState extends State { } void _onFileLoaded(File file) { - final imageProvider = Image.file( - file, - gaplessPlayback: true, - ).image; + ImageProvider imageProvider; + if (isTooLargeImage) { + _logger.info( + "Handling very large image (${_photo.width}x${_photo.height}) to prevent crash", + ); + final aspectRatio = _photo.width / _photo.height; + int targetWidth, targetHeight; + if (aspectRatio > 1) { + targetWidth = 4096; + targetHeight = (targetWidth / aspectRatio).round(); + } else { + targetHeight = 4096; + targetWidth = (targetHeight * aspectRatio).round(); + } + + imageProvider = Image.file( + file, + gaplessPlayback: true, + cacheWidth: targetWidth, + cacheHeight: targetHeight, + ).image; + } else { + imageProvider = Image.file( + file, + gaplessPlayback: true, + ).image; + } if (mounted) { precacheImage( @@ -398,6 +428,9 @@ class _ZoomableImageState extends State { _loadedFinalImage = true; _logger.info("Final image loaded"); }); + if (_imageProvider != null) { + widget.onFinalFileLoad?.call(5); + } } Future _updatePhotoViewController({ @@ -435,8 +468,31 @@ class _ZoomableImageState extends State { _logger.info("Compressing ${_photo.displayName} to viewable format"); _convertToSupportedFormat = true; - final compressedFile = - await FlutterImageCompress.compressWithFile(file.path); + Uint8List? compressedFile; + if (isTooLargeImage) { + _logger.info( + "Compressing very large image (${_photo.width}x${_photo.height}) more aggressively", + ); + final aspectRatio = _photo.width / _photo.height; + int targetWidth, targetHeight; + + if (aspectRatio > 1) { + targetWidth = 4096; + targetHeight = (targetWidth / aspectRatio).round(); + } else { + targetHeight = 4096; + targetWidth = (targetHeight * aspectRatio).round(); + } + + compressedFile = await FlutterImageCompress.compressWithFile( + file.path, + minWidth: targetWidth, + minHeight: targetHeight, + quality: 85, + ); + } else { + compressedFile = await FlutterImageCompress.compressWithFile(file.path); + } if (compressedFile != null) { final imageProvider = MemoryImage(compressedFile); diff --git a/mobile/lib/ui/viewer/file/zoomable_live_image_new.dart b/mobile/lib/ui/viewer/file/zoomable_live_image_new.dart index 65f05dbdd0..8a85cbf949 100644 --- a/mobile/lib/ui/viewer/file/zoomable_live_image_new.dart +++ b/mobile/lib/ui/viewer/file/zoomable_live_image_new.dart @@ -23,6 +23,8 @@ class ZoomableLiveImageNew extends StatefulWidget { final Function(bool)? shouldDisableScroll; final String? tagPrefix; final Decoration? backgroundDecoration; + final bool isFromMemories; + final Function(int)? onFinalFileLoad; const ZoomableLiveImageNew( this.enteFile, { @@ -30,6 +32,8 @@ class ZoomableLiveImageNew extends StatefulWidget { this.shouldDisableScroll, required this.tagPrefix, this.backgroundDecoration, + this.isFromMemories = false, + this.onFinalFileLoad, }); @override @@ -94,13 +98,17 @@ class _ZoomableLiveImageNewState extends State shouldDisableScroll: widget.shouldDisableScroll, backgroundDecoration: widget.backgroundDecoration, isGuestView: isGuestView, + onFinalFileLoad: widget.onFinalFileLoad, ); } - return GestureDetector( - onLongPressStart: (_) => {_onLongPressEvent(true)}, - onLongPressEnd: (_) => {_onLongPressEvent(false)}, - child: content, - ); + if (!widget.isFromMemories) { + return GestureDetector( + onLongPressStart: (_) => _onLongPressEvent(true), + onLongPressEnd: (_) => _onLongPressEvent(false), + child: content, + ); + } + return content; } @override diff --git a/mobile/lib/ui/viewer/file_details/added_by_widget.dart b/mobile/lib/ui/viewer/file_details/added_by_widget.dart index 12f9a0a528..f1009f07e5 100644 --- a/mobile/lib/ui/viewer/file_details/added_by_widget.dart +++ b/mobile/lib/ui/viewer/file_details/added_by_widget.dart @@ -16,11 +16,6 @@ class AddedByWidget extends StatelessWidget { if (!file.isUploaded) { return const SizedBox.shrink(); } - final bool fileIsFromSharedPublicLink = - CollectionsService.instance.isSharedPublicLink(file.collectionID!); - if (fileIsFromSharedPublicLink) { - return const SizedBox.shrink(); - } String? addedBy; if (file.isOwner && file.isCollect) { addedBy = file.uploaderName; diff --git a/mobile/lib/ui/viewer/file_details/face_widget.dart b/mobile/lib/ui/viewer/file_details/face_widget.dart deleted file mode 100644 index b841b872c0..0000000000 --- a/mobile/lib/ui/viewer/file_details/face_widget.dart +++ /dev/null @@ -1,310 +0,0 @@ -import "dart:async"; -import "dart:developer" show log; -import "dart:typed_data"; - -import "package:flutter/cupertino.dart"; -import "package:flutter/foundation.dart" show kDebugMode; -import "package:flutter/material.dart"; -import "package:logging/logging.dart"; -import "package:photos/db/ml/db.dart"; -import "package:photos/generated/l10n.dart"; -import "package:photos/models/base/id.dart"; -import 'package:photos/models/file/file.dart'; -import "package:photos/models/ml/face/face.dart"; -import "package:photos/models/ml/face/person.dart"; -import "package:photos/services/machine_learning/face_ml/face_detection/detection.dart"; -import "package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart"; -import "package:photos/services/machine_learning/face_ml/feedback/cluster_feedback.dart"; -import "package:photos/services/machine_learning/ml_service.dart"; -import "package:photos/services/search_service.dart"; -import "package:photos/theme/ente_theme.dart"; -import "package:photos/ui/notification/toast.dart"; -import "package:photos/ui/viewer/file/no_thumbnail_widget.dart"; -import "package:photos/ui/viewer/people/cluster_page.dart"; -import "package:photos/ui/viewer/people/people_page.dart"; -import "package:photos/utils/face/face_box_crop.dart"; - -class FaceWidget extends StatefulWidget { - final EnteFile file; - final Face face; - final Future?>? faceCrops; - final PersonEntity? person; - final String? clusterID; - final bool highlight; - final bool editMode; - - const FaceWidget( - this.file, - this.face, { - this.faceCrops, - this.person, - this.clusterID, - this.highlight = false, - this.editMode = false, - super.key, - }); - - @override - State createState() => _FaceWidgetState(); -} - -class _FaceWidgetState extends State { - bool isJustRemoved = false; - - final _logger = Logger("FaceWidget"); - - @override - Widget build(BuildContext context) { - final bool givenFaces = widget.faceCrops != null; - return _buildFaceImageGenerated(givenFaces); - } - - Widget _buildFaceImageGenerated(bool givenFaces) { - late final mlDataDB = MLDataDB.instance; - return FutureBuilder?>( - future: givenFaces - ? widget.faceCrops - : getCachedFaceCrops(widget.file, [widget.face]), - builder: (context, snapshot) { - if (snapshot.hasData) { - final ImageProvider imageProvider = - MemoryImage(snapshot.data![widget.face.faceID]!); - - return GestureDetector( - onTap: () async { - if (widget.editMode) return; - - log( - "FaceWidget is tapped, with person ${widget.person?.data.name} and clusterID ${widget.clusterID}", - name: "FaceWidget", - ); - if (widget.person == null && widget.clusterID == null) { - // Double check that it doesn't belong to an existing clusterID. - final existingClusterID = - await mlDataDB.getClusterIDForFaceID(widget.face.faceID); - if (existingClusterID != null) { - final fileIdsToClusterIds = - await mlDataDB.getFileIdToClusterIds(); - final files = - await SearchService.instance.getAllFilesForSearch(); - final clusterFiles = files - .where( - (file) => - fileIdsToClusterIds[file.uploadedFileID] - ?.contains(existingClusterID) ?? - false, - ) - .toList(); - await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ClusterPage( - clusterFiles, - clusterID: existingClusterID, - ), - ), - ); - return; - } - if (widget.face.score <= kMinimumQualityFaceScore) { - // The face score is too low for automatic clustering, - // assigning a manual new clusterID so that the user can cluster it manually - final String clusterID = newClusterID(); - await mlDataDB.updateFaceIdToClusterId( - {widget.face.faceID: clusterID}, - ); - await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ClusterPage( - [widget.file], - clusterID: clusterID, - ), - ), - ); - return; - } - - showShortToast( - context, - S.of(context).faceNotClusteredYet, - ); - unawaited(MLService.instance.clusterAllImages(force: true)); - return; - } - if (widget.person != null) { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => PeoplePage( - person: widget.person!, - searchResult: null, - ), - ), - ); - } else if (widget.clusterID != null) { - final fileIdsToClusterIds = - await mlDataDB.getFileIdToClusterIds(); - final files = - await SearchService.instance.getAllFilesForSearch(); - final clusterFiles = files - .where( - (file) => - fileIdsToClusterIds[file.uploadedFileID] - ?.contains(widget.clusterID) ?? - false, - ) - .toList(); - await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ClusterPage( - clusterFiles, - clusterID: widget.clusterID!, - ), - ), - ); - } - }, - child: Column( - children: [ - Stack( - children: [ - Container( - height: 60, - width: 60, - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all( - Radius.elliptical(16, 12), - ), - side: widget.highlight - ? BorderSide( - color: getEnteColorScheme(context).primary700, - width: 1.0, - ) - : BorderSide.none, - ), - ), - child: ClipRRect( - borderRadius: - const BorderRadius.all(Radius.elliptical(16, 12)), - child: SizedBox( - width: 60, - height: 60, - child: Image( - image: imageProvider, - fit: BoxFit.cover, - ), - ), - ), - ), - // TODO: the edges of the green line are still not properly rounded around ClipRRect - if (widget.editMode) - Positioned( - right: 0, - top: 0, - child: GestureDetector( - onTap: _cornerIconPressed, - child: isJustRemoved - ? const Icon( - CupertinoIcons.add_circled_solid, - color: Colors.green, - ) - : const Icon( - Icons.cancel, - color: Colors.red, - ), - ), - ), - ], - ), - const SizedBox(height: 8), - if (widget.person != null) - Text( - widget.person!.data.isIgnored - ? '(' + S.of(context).ignored + ')' - : widget.person!.data.name.trim(), - style: Theme.of(context).textTheme.bodySmall, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - if (kDebugMode) - Text( - 'S: ${widget.face.score.toStringAsFixed(3)}', - style: Theme.of(context).textTheme.bodySmall, - maxLines: 1, - ), - if (kDebugMode) - Text( - 'B: ${widget.face.blur.toStringAsFixed(0)}', - style: Theme.of(context).textTheme.bodySmall, - maxLines: 1, - ), - if (kDebugMode) - Text( - 'D: ${widget.face.detection.getFaceDirection().toDirectionString()}', - style: Theme.of(context).textTheme.bodySmall, - maxLines: 1, - ), - if (kDebugMode) - Text( - 'Sideways: ${widget.face.detection.faceIsSideways().toString()}', - style: Theme.of(context).textTheme.bodySmall, - maxLines: 1, - ), - if (kDebugMode && widget.face.score < 0.75) - Text( - '[Debug only]', - style: Theme.of(context).textTheme.bodySmall, - maxLines: 1, - ), - ], - ), - ); - } else { - if (snapshot.connectionState == ConnectionState.waiting) { - return const ClipRRect( - borderRadius: BorderRadius.all(Radius.elliptical(16, 12)), - child: SizedBox( - width: 60, - height: 60, - child: CircularProgressIndicator(), - ), - ); - } - if (snapshot.hasError) { - _logger.severe( - 'Error getting face: ${snapshot.error} ${snapshot.stackTrace}', - ); - } - return const ClipRRect( - borderRadius: BorderRadius.all(Radius.elliptical(16, 12)), - child: SizedBox( - width: 60, - height: 60, - child: NoThumbnailWidget(), - ), - ); - } - }, - ); - } - - void _cornerIconPressed() async { - log('face widget (file info) corner icon is pressed'); - try { - if (isJustRemoved) { - await ClusterFeedbackService.instance - .addFacesToCluster([widget.face.faceID], widget.clusterID!); - } else { - await ClusterFeedbackService.instance - .removeFilesFromCluster([widget.file], widget.clusterID!); - } - - setState(() { - isJustRemoved = !isJustRemoved; - }); - } catch (e, s) { - _logger.severe( - "removing face/file from cluster from file info widget failed: $e, \n $s", - ); - } - } -} diff --git a/mobile/lib/ui/viewer/file_details/file_info_face_widget.dart b/mobile/lib/ui/viewer/file_details/file_info_face_widget.dart new file mode 100644 index 0000000000..14e036240a --- /dev/null +++ b/mobile/lib/ui/viewer/file_details/file_info_face_widget.dart @@ -0,0 +1,207 @@ +import "dart:async"; +import "dart:typed_data"; + +import "package:flutter/foundation.dart" show kDebugMode; +import "package:flutter/material.dart"; +import "package:photos/db/ml/db.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/models/base/id.dart"; +import 'package:photos/models/file/file.dart'; +import "package:photos/models/ml/face/face.dart"; +import "package:photos/models/ml/face/person.dart"; +import "package:photos/services/machine_learning/face_ml/face_detection/detection.dart"; +import "package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart"; +import "package:photos/services/machine_learning/ml_service.dart"; +import "package:photos/services/search_service.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/notification/toast.dart"; +import "package:photos/ui/viewer/people/cluster_page.dart"; +import "package:photos/ui/viewer/people/file_face_widget.dart"; +import "package:photos/ui/viewer/people/people_page.dart"; + +class FileInfoFaceWidget extends StatefulWidget { + final EnteFile file; + final Face face; + final Uint8List faceCrop; + final PersonEntity? person; + final String? clusterID; + final bool highlight; + + const FileInfoFaceWidget( + this.file, + this.face, { + required this.faceCrop, + this.person, + this.clusterID, + this.highlight = false, + super.key, + }); + + @override + State createState() => _FileInfoFaceWidgetState(); +} + +class _FileInfoFaceWidgetState extends State { + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: _routeToPersonOrClusterPage, + child: Column( + children: [ + Stack( + children: [ + Container( + height: 60, + width: 60, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all( + Radius.elliptical(16, 12), + ), + side: widget.highlight + ? BorderSide( + color: getEnteColorScheme(context).primary700, + width: 1.0, + ) + : BorderSide.none, + ), + ), + child: ClipRRect( + borderRadius: + const BorderRadius.all(Radius.elliptical(16, 12)), + child: SizedBox( + width: 60, + height: 60, + child: FileFaceWidget( + widget.file, + faceCrop: widget.faceCrop, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 8), + ..._buildFaceInfo(), + ], + ), + ); + } + + List _buildFaceInfo() { + final List faceInfo = []; + if (widget.person != null) { + faceInfo.add( + Text( + widget.person!.data.isIgnored + ? '(' + S.of(context).ignored + ')' + : widget.person!.data.name.trim(), + style: Theme.of(context).textTheme.bodySmall, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ); + } + if (kDebugMode) { + faceInfo.add( + Text( + 'S: ${widget.face.score.toStringAsFixed(3)} (I)', + style: Theme.of(context).textTheme.bodySmall, + maxLines: 1, + ), + ); + faceInfo.add( + Text( + 'B: ${widget.face.blur.toStringAsFixed(0)} (I)', + style: Theme.of(context).textTheme.bodySmall, + maxLines: 1, + ), + ); + faceInfo.add( + Text( + 'D: ${widget.face.detection.getFaceDirection().toDirectionString()} (I)', + style: Theme.of(context).textTheme.bodySmall, + maxLines: 1, + ), + ); + faceInfo.add( + Text( + 'Sideways: ${widget.face.detection.faceIsSideways().toString()} (I)', + style: Theme.of(context).textTheme.bodySmall, + maxLines: 1, + ), + ); + if (widget.face.score < kMinimumFaceShowScore) { + faceInfo.add( + Text( + 'Not visible to user (I)', + style: Theme.of(context).textTheme.bodySmall, + maxLines: 1, + ), + ); + } + } + return faceInfo; + } + + Future _routeToPersonOrClusterPage() async { + final mlDataDB = MLDataDB.instance; + if (widget.person != null) { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => PeoplePage( + person: widget.person!, + searchResult: null, + ), + ), + ); + return; + } + final String? clusterID = widget.clusterID ?? + await mlDataDB.getClusterIDForFaceID(widget.face.faceID); + if (clusterID != null) { + final fileIdsToClusterIds = await mlDataDB.getFileIdToClusterIds(); + final files = await SearchService.instance.getAllFilesForSearch(); + final clusterFiles = files + .where( + (file) => + fileIdsToClusterIds[file.uploadedFileID]?.contains(clusterID) ?? + false, + ) + .toList(); + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ClusterPage( + clusterFiles, + clusterID: clusterID, + ), + ), + ); + return; + } + if (widget.face.score <= kMinimumQualityFaceScore) { + // The face score is too low for automatic clustering, + // assigning a manual new clusterID so that the user can cluster it manually + final String clusterID = newClusterID(); + await mlDataDB.updateFaceIdToClusterId( + {widget.face.faceID: clusterID}, + ); + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ClusterPage( + [widget.file], + clusterID: clusterID, + ), + ), + ); + return; + } + + showShortToast( + context, + S.of(context).faceNotClusteredYet, + ); + unawaited(MLService.instance.clusterAllImages(force: true)); + return; + } +} diff --git a/mobile/lib/ui/viewer/file_details/faces_item_widget.dart b/mobile/lib/ui/viewer/file_details/file_info_faces_item_widget.dart similarity index 58% rename from mobile/lib/ui/viewer/file_details/faces_item_widget.dart rename to mobile/lib/ui/viewer/file_details/file_info_faces_item_widget.dart index 25b7bf6087..5f94ee8421 100644 --- a/mobile/lib/ui/viewer/file_details/faces_item_widget.dart +++ b/mobile/lib/ui/viewer/file_details/file_info_faces_item_widget.dart @@ -6,12 +6,13 @@ import "package:photos/generated/l10n.dart"; import "package:photos/models/file/file.dart"; import "package:photos/models/ml/face/face.dart"; import "package:photos/models/ml/face/person.dart"; +import "package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart"; import "package:photos/services/machine_learning/face_ml/feedback/cluster_feedback.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import "package:photos/ui/components/buttons/chip_button_widget.dart"; import "package:photos/ui/components/info_item_widget.dart"; -import "package:photos/ui/viewer/file_details/face_widget.dart"; -import "package:photos/utils/face/face_box_crop.dart"; +import "package:photos/ui/viewer/file_details/file_info_face_widget.dart"; +import "package:photos/utils/face/face_thumbnail_cache.dart"; final Logger _logger = Logger("FacesItemWidget"); @@ -24,8 +25,6 @@ class FacesItemWidget extends StatefulWidget { } class _FacesItemWidgetState extends State { - bool editMode = false; - @override void initState() { super.initState(); @@ -37,53 +36,43 @@ class _FacesItemWidgetState extends State { return InfoItemWidget( key: const ValueKey("Faces"), leadingIcon: Icons.face_retouching_natural_outlined, - subtitleSection: _faceWidgets(context, widget.file, editMode), + subtitleSection: _faceWidgets(context, widget.file), hasChipButtons: true, biggerSpinner: true, ); } - Future> _faceWidgets( - BuildContext context, - EnteFile file, - bool editMode, - ) async { - late final mlDataDB = MLDataDB.instance; + Future> _faceWidgets(BuildContext context, EnteFile file) async { + final mlDataDB = MLDataDB.instance; try { if (file.uploadedFileID == null) { - return [ - ChipButtonWidget( - S.of(context).fileNotUploadedYet, - noChips: true, - ), - ]; + return [const NoFaceChipButtonWidget(NoFacesReason.fileNotUploaded)]; } final List? faces = await mlDataDB.getFacesForGivenFileID(file.uploadedFileID!); if (faces == null) { - return [ - ChipButtonWidget( - S.of(context).imageNotAnalyzed, - noChips: true, - ), - ]; + return [const NoFaceChipButtonWidget(NoFacesReason.fileNotAnalyzed)]; } // Remove faces with low scores if (!kDebugMode) { - faces.removeWhere((face) => (face.score < 0.75)); + final beforeLength = faces.length; + final lowScores = faces + .where((face) => (face.score < kMinimumFaceShowScore)) + .toList(); + faces.removeWhere((face) => (face.score < kMinimumFaceShowScore)); + if (faces.length != beforeLength) { + _logger.warning( + 'File ${file.uploadedFileID} has ${beforeLength - faces.length} faces with low scores ($lowScores) that are not shown in the UI', + ); + } } else { faces.removeWhere((face) => (face.score < 0.5)); } if (faces.isEmpty) { - return [ - ChipButtonWidget( - S.of(context).noFacesFound, - noChips: true, - ), - ]; + return [const NoFaceChipButtonWidget(NoFacesReason.noFacesFound)]; } final faceIdsToClusterIds = await mlDataDB @@ -131,14 +120,23 @@ class _FacesItemWidgetState extends State { final lastViewedClusterID = ClusterFeedbackService.lastViewedClusterID; - final faceWidgets = []; + final faceWidgets = []; - // await generation of the face crops here, so that the file info shows one central loading spinner - final _ = await getCachedFaceCrops(file, faces); - - final faceCrops = getCachedFaceCrops(file, faces); + final faceCrops = await getCachedFaceCrops(file, faces); final List faceIDs = []; + final List faceScores = []; for (final Face face in faces) { + final faceCrop = faceCrops != null ? faceCrops[face.faceID] : null; + if (faceCrop == null) { + _logger.severe( + 'Face crop for face ${face.faceID} in file ${file.uploadedFileID} is null, skipping face widget.', + ); + return [ + const NoFaceChipButtonWidget( + NoFacesReason.faceThumbnailGenerationFailed, + ), + ]; + } final String? clusterID = faceIdsToClusterIds[face.faceID]; final PersonEntity? person = clusterIDToPerson[clusterID] != null ? persons[clusterIDToPerson[clusterID]!] @@ -146,25 +144,70 @@ class _FacesItemWidgetState extends State { final highlight = (clusterID == lastViewedClusterID) && (person == null); faceIDs.add(face.faceID); + faceScores.add(face.score); faceWidgets.add( - FaceWidget( + FileInfoFaceWidget( file, face, - faceCrops: faceCrops, + faceCrop: faceCrop, clusterID: clusterID, person: person, highlight: highlight, - editMode: highlight ? editMode : false, ), ); } - _logger.info('File ${file.uploadedFileID} has FaceIDs: $faceIDs'); + _logger.info( + 'File ${file.uploadedFileID} has FaceIDs: $faceIDs with scores: $faceScores', + ); return faceWidgets; } catch (e, s) { _logger.severe('failed to get face widgets in file info', e, s); - return []; + return []; } } } + +enum NoFacesReason { + fileNotUploaded, + fileNotAnalyzed, + noFacesFound, + faceThumbnailGenerationFailed, +} + +String getNoFaceReasonText( + BuildContext context, + NoFacesReason reason, +) { + switch (reason) { + case NoFacesReason.fileNotUploaded: + return S.of(context).fileNotUploadedYet; + case NoFacesReason.fileNotAnalyzed: + return S.of(context).imageNotAnalyzed; + case NoFacesReason.noFacesFound: + return S.of(context).noFacesFound; + case NoFacesReason.faceThumbnailGenerationFailed: + return "Unable to generate face thumbnails"; + } +} + +class NoFaceChipButtonWidget extends StatelessWidget { + final NoFacesReason reason; + + const NoFaceChipButtonWidget( + this.reason, { + super.key, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 5), + child: ChipButtonWidget( + getNoFaceReasonText(context, reason), + noChips: true, + ), + ); + } +} diff --git a/mobile/lib/ui/viewer/gallery/component/gallery_file_widget.dart b/mobile/lib/ui/viewer/gallery/component/gallery_file_widget.dart index 178744c7d9..1462dbf8ae 100644 --- a/mobile/lib/ui/viewer/gallery/component/gallery_file_widget.dart +++ b/mobile/lib/ui/viewer/gallery/component/gallery_file_widget.dart @@ -6,7 +6,6 @@ import "package:photos/core/constants.dart"; import 'package:photos/models/file/file.dart'; import "package:photos/models/selected_files.dart"; import "package:photos/services/app_lifecycle_service.dart"; -import "package:photos/services/collections_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/viewer/file/detail_page.dart"; import "package:photos/ui/viewer/file/thumbnail_widget.dart"; @@ -38,16 +37,9 @@ class GalleryFileWidget extends StatelessWidget { @override Widget build(BuildContext context) { final isFileSelected = selectedFiles?.isFileSelected(file) ?? false; - bool fileIsFromSharedPublicLink = false; - if (file.collectionID != null) { - fileIsFromSharedPublicLink = - CollectionsService.instance.isSharedPublicLink(file.collectionID!); - } + Color selectionColor = Colors.white; - if (isFileSelected && - file.isUploaded && - file.ownerID != currentUserID && - !fileIsFromSharedPublicLink) { + if (isFileSelected && file.isUploaded && file.ownerID != currentUserID) { final avatarColors = getEnteColorScheme(context).avatarColors; selectionColor = avatarColors[(file.ownerID!).remainder(avatarColors.length)]; @@ -62,7 +54,7 @@ class GalleryFileWidget extends StatelessWidget { thumbnailSize: photoGridSize < photoGridSizeDefault ? thumbnailLargeSize : thumbnailSmallSize, - shouldShowOwnerAvatar: !(isFileSelected || fileIsFromSharedPublicLink), + shouldShowOwnerAvatar: !isFileSelected, shouldShowVideoDuration: true, ); return GestureDetector( diff --git a/mobile/lib/ui/viewer/gallery/hierarchical_search_gallery.dart b/mobile/lib/ui/viewer/gallery/hierarchical_search_gallery.dart index 69bcf8d03c..48621a0f73 100644 --- a/mobile/lib/ui/viewer/gallery/hierarchical_search_gallery.dart +++ b/mobile/lib/ui/viewer/gallery/hierarchical_search_gallery.dart @@ -20,7 +20,7 @@ import "package:photos/ui/viewer/gallery/state/search_filter_data_provider.dart" import "package:photos/ui/viewer/people/add_person_action_sheet.dart"; import "package:photos/ui/viewer/people/people_banner.dart"; import "package:photos/ui/viewer/people/people_page.dart"; -import "package:photos/ui/viewer/search/result/person_face_widget.dart"; +import "package:photos/ui/viewer/people/person_face_widget.dart"; import "package:photos/utils/hierarchical_search_util.dart"; import "package:photos/utils/navigation_util.dart"; @@ -171,11 +171,8 @@ class _HierarchicalSearchGalleryState extends State { ? PeopleBanner( type: PeopleBannerType.addName, faceWidget: PersonFaceWidget( - _firstUnnamedAppliedFaceFilter!.faceFile, clusterID: _firstUnnamedAppliedFaceFilter!.clusterId, - thumbnailFallback: false, - cannotTrustFile: true, ), actionIcon: Icons.add_outlined, text: S.of(context).savePerson, diff --git a/mobile/lib/ui/viewer/gallery/hooks/pick_cover_photo.dart b/mobile/lib/ui/viewer/gallery/hooks/pick_cover_photo.dart index f6b8978306..4a0b116ff7 100644 --- a/mobile/lib/ui/viewer/gallery/hooks/pick_cover_photo.dart +++ b/mobile/lib/ui/viewer/gallery/hooks/pick_cover_photo.dart @@ -37,7 +37,7 @@ Future showPickCoverPhotoSheet( topControl: const SizedBox.shrink(), backgroundColor: getEnteColorScheme(context).backgroundElevated, barrierColor: backdropFaintDark, - enableDrag: false, + enableDrag: true, ); } diff --git a/mobile/lib/ui/viewer/gallery/hooks/pick_person_avatar.dart b/mobile/lib/ui/viewer/gallery/hooks/pick_person_avatar.dart index 9d7dabbda6..1adecdee72 100644 --- a/mobile/lib/ui/viewer/gallery/hooks/pick_person_avatar.dart +++ b/mobile/lib/ui/viewer/gallery/hooks/pick_person_avatar.dart @@ -38,7 +38,7 @@ Future showPersonAvatarPhotoSheet( topControl: const SizedBox.shrink(), backgroundColor: getEnteColorScheme(context).backgroundElevated, barrierColor: backdropFaintDark, - enableDrag: false, + enableDrag: true, ); } @@ -137,57 +137,36 @@ class PickPersonCoverPhotoWidget extends StatelessWidget { ), ), ), - child: Column( - children: [ - ValueListenableBuilder( - valueListenable: isFileSelected, - builder: (context, bool value, _) { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - switchInCurve: Curves.easeInOutExpo, - switchOutCurve: Curves.easeInOutExpo, - child: ButtonWidget( - key: ValueKey(value), - isDisabled: !value, - buttonType: ButtonType.neutral, - labelText: S.of(context).useSelectedPhoto, - onTap: () async { - final selectedFile = - selectedFiles.files.first; - final result = await PersonService.instance - .updateAvatar( - personEntity, - selectedFile, - ); - Bus.instance.fire( - PeopleChangedEvent( - type: PeopleEventType.saveOrEditPerson, - person: result, - ), - ); - Navigator.pop(context, result); - }, - ), - ); - }, - ), - const SizedBox(height: 8), - ButtonWidget( - buttonType: ButtonType.secondary, - buttonAction: ButtonAction.cancel, - labelText: S.of(context).cancel, - // labelText: collection.hasCover - // ? S.of(context).resetToDefault - // : S.of(context).cancel, - icon: null, - // icon: collection.hasCover - // ? Icons.restore_outlined - // : null, - onTap: () async { - Navigator.of(context).pop(); - }, - ), - ], + child: ValueListenableBuilder( + valueListenable: isFileSelected, + builder: (context, bool value, _) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + switchInCurve: Curves.easeInOutExpo, + switchOutCurve: Curves.easeInOutExpo, + child: ButtonWidget( + key: ValueKey(value), + isDisabled: !value, + buttonType: ButtonType.neutral, + labelText: S.of(context).useSelectedPhoto, + onTap: () async { + final selectedFile = selectedFiles.files.first; + final result = + await PersonService.instance.updateAvatar( + personEntity, + selectedFile, + ); + Bus.instance.fire( + PeopleChangedEvent( + type: PeopleEventType.saveOrEditPerson, + person: result, + ), + ); + Navigator.pop(context, result); + }, + ), + ); + }, ), ), ), diff --git a/mobile/lib/ui/viewer/hierarchicial_search/applied_filters_for_appbar.dart b/mobile/lib/ui/viewer/hierarchicial_search/applied_filters_for_appbar.dart index bc41ed77af..869df13bca 100644 --- a/mobile/lib/ui/viewer/hierarchicial_search/applied_filters_for_appbar.dart +++ b/mobile/lib/ui/viewer/hierarchicial_search/applied_filters_for_appbar.dart @@ -66,7 +66,6 @@ class _AppliedFiltersForAppbarState extends State { ? FaceFilterChip( personId: filter.personId, clusterId: filter.clusterId, - faceThumbnailFile: filter.faceFile, apply: () { _searchFilterDataProvider.applyFilters([filter]); }, diff --git a/mobile/lib/ui/viewer/hierarchicial_search/chip_widgets/face_filter_chip.dart b/mobile/lib/ui/viewer/hierarchicial_search/chip_widgets/face_filter_chip.dart index 67d99be854..b7f36eaf2d 100644 --- a/mobile/lib/ui/viewer/hierarchicial_search/chip_widgets/face_filter_chip.dart +++ b/mobile/lib/ui/viewer/hierarchicial_search/chip_widgets/face_filter_chip.dart @@ -1,13 +1,11 @@ import "package:flutter/material.dart"; import "package:photos/core/constants.dart"; -import "package:photos/models/file/file.dart"; import "package:photos/theme/ente_theme.dart"; -import "package:photos/ui/viewer/search/result/person_face_widget.dart"; +import "package:photos/ui/viewer/people/person_face_widget.dart"; class FaceFilterChip extends StatefulWidget { final String? personId; final String? clusterId; - final EnteFile faceThumbnailFile; final VoidCallback apply; final VoidCallback remove; final bool isApplied; @@ -16,7 +14,6 @@ class FaceFilterChip extends StatefulWidget { const FaceFilterChip({ required this.personId, required this.clusterId, - required this.faceThumbnailFile, required this.apply, required this.remove, required this.isApplied, @@ -72,12 +69,9 @@ class _FaceFilterChipState extends State { width: kFilterChipHeight * scale, height: kFilterChipHeight * scale, child: PersonFaceWidget( - widget.faceThumbnailFile, personId: widget.personId, clusterID: widget.clusterId, - thumbnailFallback: false, useFullFile: false, - cannotTrustFile: true, ), ), ), diff --git a/mobile/lib/ui/viewer/hierarchicial_search/chip_widgets/only_them_filter_chip.dart b/mobile/lib/ui/viewer/hierarchicial_search/chip_widgets/only_them_filter_chip.dart index b3b00091d3..85c616602e 100644 --- a/mobile/lib/ui/viewer/hierarchicial_search/chip_widgets/only_them_filter_chip.dart +++ b/mobile/lib/ui/viewer/hierarchicial_search/chip_widgets/only_them_filter_chip.dart @@ -3,7 +3,7 @@ import "package:photos/core/constants.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/models/search/hierarchical/face_filter.dart"; import "package:photos/theme/ente_theme.dart"; -import "package:photos/ui/viewer/search/result/person_face_widget.dart"; +import "package:photos/ui/viewer/people/person_face_widget.dart"; class OnlyThemFilterChip extends StatelessWidget { final List faceFilters; @@ -88,12 +88,9 @@ class _OnlyThemFilterThumbnail extends StatelessWidget { width: kFilterChipHeight, height: kFilterChipHeight, child: PersonFaceWidget( - faceFilters.first.faceFile, personId: faceFilters.first.personId, clusterID: faceFilters.first.clusterId, - thumbnailFallback: false, useFullFile: false, - cannotTrustFile: true, ), ), ); @@ -105,12 +102,9 @@ class _OnlyThemFilterThumbnail extends StatelessWidget { width: kFilterChipHeight / 2, height: kFilterChipHeight, child: PersonFaceWidget( - faceFilters.first.faceFile, personId: faceFilters.first.personId, clusterID: faceFilters.first.clusterId, - thumbnailFallback: false, useFullFile: false, - cannotTrustFile: true, ), ), const SizedBox(width: 1), @@ -118,12 +112,9 @@ class _OnlyThemFilterThumbnail extends StatelessWidget { width: kFilterChipHeight / 2, height: kFilterChipHeight, child: PersonFaceWidget( - faceFilters.last.faceFile, personId: faceFilters.last.personId, clusterID: faceFilters.last.clusterId, - thumbnailFallback: false, useFullFile: false, - cannotTrustFile: true, ), ), ], @@ -138,12 +129,9 @@ class _OnlyThemFilterThumbnail extends StatelessWidget { height: kFilterChipHeight, width: kFilterChipHeight / 2 - 0.5, child: PersonFaceWidget( - faceFilters[0].faceFile, personId: faceFilters[0].personId, clusterID: faceFilters[0].clusterId, - thumbnailFallback: false, useFullFile: false, - cannotTrustFile: true, ), ), const SizedBox(width: 1), @@ -158,12 +146,9 @@ class _OnlyThemFilterThumbnail extends StatelessWidget { bottomLeft: Radius.circular(1), ), child: PersonFaceWidget( - faceFilters[1].faceFile, personId: faceFilters[1].personId, clusterID: faceFilters[1].clusterId, - thumbnailFallback: false, useFullFile: false, - cannotTrustFile: true, ), ), ), @@ -176,12 +161,9 @@ class _OnlyThemFilterThumbnail extends StatelessWidget { topLeft: Radius.circular(1), ), child: PersonFaceWidget( - faceFilters[2].faceFile, personId: faceFilters[2].personId, clusterID: faceFilters[2].clusterId, - thumbnailFallback: false, useFullFile: false, - cannotTrustFile: true, ), ), ), @@ -206,12 +188,9 @@ class _OnlyThemFilterThumbnail extends StatelessWidget { bottomRight: Radius.circular(1), ), child: PersonFaceWidget( - faceFilters[0].faceFile, personId: faceFilters[0].personId, clusterID: faceFilters[0].clusterId, - thumbnailFallback: false, useFullFile: false, - cannotTrustFile: true, ), ), ), @@ -224,12 +203,9 @@ class _OnlyThemFilterThumbnail extends StatelessWidget { bottomLeft: Radius.circular(1), ), child: PersonFaceWidget( - faceFilters[1].faceFile, personId: faceFilters[1].personId, clusterID: faceFilters[1].clusterId, - thumbnailFallback: false, useFullFile: false, - cannotTrustFile: true, ), ), ), @@ -247,12 +223,9 @@ class _OnlyThemFilterThumbnail extends StatelessWidget { topRight: Radius.circular(1), ), child: PersonFaceWidget( - faceFilters[2].faceFile, personId: faceFilters[2].personId, clusterID: faceFilters[2].clusterId, - thumbnailFallback: false, useFullFile: false, - cannotTrustFile: true, ), ), ), @@ -265,12 +238,9 @@ class _OnlyThemFilterThumbnail extends StatelessWidget { topLeft: Radius.circular(1), ), child: PersonFaceWidget( - faceFilters[3].faceFile, personId: faceFilters[3].personId, clusterID: faceFilters[3].clusterId, - thumbnailFallback: false, useFullFile: false, - cannotTrustFile: true, ), ), ), diff --git a/mobile/lib/ui/viewer/hierarchicial_search/filter_options_bottom_sheet.dart b/mobile/lib/ui/viewer/hierarchicial_search/filter_options_bottom_sheet.dart index bd9c245ed9..8fae99a750 100644 --- a/mobile/lib/ui/viewer/hierarchicial_search/filter_options_bottom_sheet.dart +++ b/mobile/lib/ui/viewer/hierarchicial_search/filter_options_bottom_sheet.dart @@ -57,7 +57,6 @@ class _FilterOptionsBottomSheetState extends State { FaceFilterChip( personId: filter.personId, clusterId: filter.clusterId, - faceThumbnailFile: filter.faceFile, isInAllFiltersView: true, apply: () { widget.searchFilterDataProvider diff --git a/mobile/lib/ui/viewer/hierarchicial_search/recommended_filters_for_appbar.dart b/mobile/lib/ui/viewer/hierarchicial_search/recommended_filters_for_appbar.dart index 132f3a2b2b..2c98a5bd38 100644 --- a/mobile/lib/ui/viewer/hierarchicial_search/recommended_filters_for_appbar.dart +++ b/mobile/lib/ui/viewer/hierarchicial_search/recommended_filters_for_appbar.dart @@ -121,7 +121,6 @@ class _RecommendedFiltersForAppbarState ? FaceFilterChip( personId: filter.personId, clusterId: filter.clusterId, - faceThumbnailFile: filter.faceFile, apply: () { _searchFilterDataProvider.applyFilters([filter]); }, diff --git a/mobile/lib/ui/viewer/location/add_location_sheet.dart b/mobile/lib/ui/viewer/location/add_location_sheet.dart index 29c79babad..ebcb53872f 100644 --- a/mobile/lib/ui/viewer/location/add_location_sheet.dart +++ b/mobile/lib/ui/viewer/location/add_location_sheet.dart @@ -192,13 +192,11 @@ class _AddLocationSheetState extends State { builder: (context, int? value, _) { Widget widget; if (value == null) { - widget = RepaintBoundary( - child: EnteLoadingWidget( - size: 14, - color: colorScheme.strokeMuted, - alignment: Alignment.centerLeft, - padding: 3, - ), + widget = EnteLoadingWidget( + size: 14, + color: colorScheme.strokeMuted, + alignment: Alignment.centerLeft, + padding: 3, ); } else { widget = Column( diff --git a/mobile/lib/ui/viewer/location/edit_location_sheet.dart b/mobile/lib/ui/viewer/location/edit_location_sheet.dart index 2aa071fb6d..fa9e06cf3a 100644 --- a/mobile/lib/ui/viewer/location/edit_location_sheet.dart +++ b/mobile/lib/ui/viewer/location/edit_location_sheet.dart @@ -184,13 +184,11 @@ class _EditLocationSheetState extends State { builder: (context, int? value, _) { Widget widget; if (value == null) { - widget = RepaintBoundary( - child: EnteLoadingWidget( - size: 14, - color: colorScheme.strokeMuted, - alignment: Alignment.centerLeft, - padding: 3, - ), + widget = EnteLoadingWidget( + size: 14, + color: colorScheme.strokeMuted, + alignment: Alignment.centerLeft, + padding: 3, ); } else { widget = Column( diff --git a/mobile/lib/ui/viewer/location/pick_center_point_widget.dart b/mobile/lib/ui/viewer/location/pick_center_point_widget.dart index 67234564f3..637476166a 100644 --- a/mobile/lib/ui/viewer/location/pick_center_point_widget.dart +++ b/mobile/lib/ui/viewer/location/pick_center_point_widget.dart @@ -40,7 +40,7 @@ Future showPickCenterPointSheet( topControl: const SizedBox.shrink(), backgroundColor: getEnteColorScheme(context).backgroundElevated, barrierColor: backdropFaintDark, - enableDrag: false, + enableDrag: true, ); } @@ -82,6 +82,7 @@ class PickCenterPointWidget extends StatelessWidget { title: S.of(context).pickCenterPoint, ), caption: locationTagName ?? S.of(context).newLocation, + showCloseButton: true, ), Expanded( child: GalleryFilesState( @@ -138,39 +139,26 @@ class PickCenterPointWidget extends StatelessWidget { ), ), ), - child: Column( - children: [ - ValueListenableBuilder( - valueListenable: isFileSelected, - builder: (context, bool value, _) { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - switchInCurve: Curves.easeInOutExpo, - switchOutCurve: Curves.easeInOutExpo, - child: ButtonWidget( - key: ValueKey(value), - isDisabled: !value, - buttonType: ButtonType.neutral, - labelText: S.of(context).useSelectedPhoto, - onTap: () async { - final selectedLocation = - selectedFiles.files.first.location; - Navigator.pop(context, selectedLocation); - }, - ), - ); - }, - ), - const SizedBox(height: 8), - ButtonWidget( - buttonType: ButtonType.secondary, - buttonAction: ButtonAction.cancel, - labelText: S.of(context).cancel, - onTap: () async { - Navigator.of(context).pop(); - }, - ), - ], + child: ValueListenableBuilder( + valueListenable: isFileSelected, + builder: (context, bool value, _) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + switchInCurve: Curves.easeInOutExpo, + switchOutCurve: Curves.easeInOutExpo, + child: ButtonWidget( + key: ValueKey(value), + isDisabled: !value, + buttonType: ButtonType.neutral, + labelText: S.of(context).useSelectedPhoto, + onTap: () async { + final selectedLocation = + selectedFiles.files.first.location; + Navigator.pop(context, selectedLocation); + }, + ), + ); + }, ), ), ), diff --git a/mobile/lib/ui/viewer/people/cluster_breakup_page.dart b/mobile/lib/ui/viewer/people/cluster_breakup_page.dart index c1bec40466..73df061387 100644 --- a/mobile/lib/ui/viewer/people/cluster_breakup_page.dart +++ b/mobile/lib/ui/viewer/people/cluster_breakup_page.dart @@ -3,7 +3,7 @@ import "package:photos/models/file/file.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/viewer/file/no_thumbnail_widget.dart"; import "package:photos/ui/viewer/people/cluster_page.dart"; -import "package:photos/ui/viewer/search/result/person_face_widget.dart"; +import "package:photos/ui/viewer/people/person_face_widget.dart"; class ClusterBreakupPage extends StatefulWidget { final Map> newClusterIDsToFiles; @@ -59,7 +59,6 @@ class _ClusterBreakupPageState extends State { Radius.elliptical(16, 12), ), child: PersonFaceWidget( - files.first, clusterID: clusterID, ), ) diff --git a/mobile/lib/ui/viewer/people/cluster_page.dart b/mobile/lib/ui/viewer/people/cluster_page.dart index 2b89097be9..2541892d0f 100644 --- a/mobile/lib/ui/viewer/people/cluster_page.dart +++ b/mobile/lib/ui/viewer/people/cluster_page.dart @@ -22,7 +22,7 @@ import "package:photos/ui/viewer/people/add_person_action_sheet.dart"; import "package:photos/ui/viewer/people/cluster_app_bar.dart"; import "package:photos/ui/viewer/people/people_banner.dart"; import "package:photos/ui/viewer/people/people_page.dart"; -import "package:photos/ui/viewer/search/result/person_face_widget.dart"; +import "package:photos/ui/viewer/people/person_face_widget.dart"; import "package:photos/ui/viewer/search/result/search_result_page.dart"; import "package:photos/utils/navigation_util.dart"; @@ -66,8 +66,7 @@ class _ClusterPageState extends State { files = widget.searchResult; _filesUpdatedEvent = Bus.instance.on().listen((event) { - if (event.type == EventType.deletedFromDevice || - event.type == EventType.deletedFromEverywhere || + if (event.type == EventType.deletedFromEverywhere || event.type == EventType.deletedFromRemote || event.type == EventType.hide) { for (var updatedFile in event.updatedFiles) { @@ -133,11 +132,10 @@ class _ClusterPageState extends State { selectedFiles: _selectedFiles, enableFileGrouping: widget.enableGrouping, initialFiles: widget.searchResult, - header: widget.showNamingBanner + header: widget.showNamingBanner && files.isNotEmpty ? PeopleBanner( type: PeopleBannerType.addName, faceWidget: PersonFaceWidget( - files.first, clusterID: widget.clusterID, ), actionIcon: Icons.add_outlined, diff --git a/mobile/lib/ui/viewer/people/file_face_widget.dart b/mobile/lib/ui/viewer/people/file_face_widget.dart new file mode 100644 index 0000000000..9f0346de9e --- /dev/null +++ b/mobile/lib/ui/viewer/people/file_face_widget.dart @@ -0,0 +1,131 @@ +import "dart:typed_data"; + +import 'package:flutter/widgets.dart'; +import "package:logging/logging.dart"; +import "package:photos/db/ml/db.dart"; +import 'package:photos/models/file/file.dart'; +import "package:photos/models/ml/face/face.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/common/loading_widget.dart"; +import "package:photos/ui/viewer/file/thumbnail_widget.dart"; +import "package:photos/utils/face/face_thumbnail_cache.dart"; + +final _logger = Logger("FaceWidget"); + +class FileFaceWidget extends StatefulWidget { + final EnteFile file; + + // Data to find the right face, in order of preference + final Uint8List? faceCrop; + final Face? face; + final String? clusterID; + + final bool useFullFile; + final bool thumbnailFallback; + + const FileFaceWidget( + this.file, { + this.face, + this.faceCrop, + this.clusterID, + this.useFullFile = true, + this.thumbnailFallback = false, + super.key, + }); + + @override + State createState() => _FileFaceWidgetState(); +} + +class _FileFaceWidgetState extends State { + Future? faceCropFuture; + + @override + void initState() { + super.initState(); + faceCropFuture = _getFaceCrop(); + } + + @override + void dispose() { + super.dispose(); + if (widget.faceCrop == null) { + checkStopTryingToGenerateFaceThumbnails( + widget.file.uploadedFileID!, + useFullFile: widget.useFullFile, + ); + } + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: faceCropFuture, + builder: (context, snapshot) { + if (snapshot.hasData) { + final ImageProvider imageProvider = MemoryImage(snapshot.data!); + return Stack( + fit: StackFit.expand, + children: [ + Image( + image: imageProvider, + fit: BoxFit.cover, + ), + ], + ); + } else { + if (snapshot.hasError) { + _logger.severe( + "Error getting cover face", + snapshot.error, + snapshot.stackTrace, + ); + } + return widget.thumbnailFallback + ? ThumbnailWidget(widget.file) + : EnteLoadingWidget( + color: getEnteColorScheme(context).fillMuted, + ); + } + }, + ); + } + + Future _getFaceCrop() async { + if (widget.faceCrop != null) { + return widget.faceCrop; + } + try { + final Face? faceToUse = widget.face ?? + await MLDataDB.instance.getCoverFaceForPerson( + recentFileID: widget.file.uploadedFileID!, + clusterID: widget.clusterID, + ); + if (faceToUse == null) { + _logger.severe( + "Cannot find face to crop, widget.face: ${widget.face}, clusterID: ${widget.clusterID}", + ); + } + final cropMap = await getCachedFaceCrops( + widget.file, + [faceToUse!], + useFullFile: widget.useFullFile, + ); + if (cropMap != null && cropMap[faceToUse.faceID] != null) { + return cropMap[faceToUse.faceID]; + } else { + _logger.severe( + "No face crop found for face ${faceToUse.faceID} in file ${widget.file.uploadedFileID}", + ); + return null; + } + } catch (e, s) { + _logger.severe( + "Failed to get face crop for file ${widget.file.uploadedFileID}", + e, + s, + ); + return null; + } + } +} diff --git a/mobile/lib/ui/viewer/people/people_app_bar.dart b/mobile/lib/ui/viewer/people/people_app_bar.dart index 45d1cc108d..7f733b02ad 100644 --- a/mobile/lib/ui/viewer/people/people_app_bar.dart +++ b/mobile/lib/ui/viewer/people/people_app_bar.dart @@ -17,6 +17,7 @@ import 'package:photos/services/collections_service.dart'; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; +import "package:photos/ui/viewer/gallery/hooks/pick_person_avatar.dart"; import "package:photos/ui/viewer/gallery/state/inherited_search_filter_data.dart"; import "package:photos/ui/viewer/hierarchicial_search/applied_filters_for_appbar.dart"; import "package:photos/ui/viewer/hierarchicial_search/recommended_filters_for_appbar.dart"; @@ -249,6 +250,38 @@ class _AppBarWidgetState extends State { ], ), ), + PopupMenuItem( + value: PeoplePopupAction.setCover, + child: Row( + children: [ + const Icon(Icons.image_outlined), + const Padding( + padding: EdgeInsets.all(8), + ), + Text( + S.of(context).setCover, + style: textTheme.bodyBold, + ), + ], + ), + ), + if (widget.person.data.email != null && + (widget.person.data.email == Configuration.instance.getEmail())) + PopupMenuItem( + value: PeoplePopupAction.reassignMe, + child: Row( + children: [ + const Icon(Icons.person_2_outlined), + const Padding( + padding: EdgeInsets.all(8), + ), + Text( + context.l10n.reassignMe, + style: textTheme.bodyBold, + ), + ], + ), + ), PopupMenuItem( value: PeoplePopupAction.removeLabel, child: Row( @@ -264,23 +297,6 @@ class _AppBarWidgetState extends State { ], ), ), - if (widget.person.data.email != null && - (widget.person.data.email == Configuration.instance.getEmail())) - PopupMenuItem( - value: PeoplePopupAction.reassignMe, - child: Row( - children: [ - const Icon(Icons.delete_outline), - const Padding( - padding: EdgeInsets.all(8), - ), - Text( - context.l10n.reassignMe, - style: textTheme.bodyBold, - ), - ], - ), - ), ], ); } else { @@ -394,13 +410,25 @@ class _AppBarWidgetState extends State { } Future setCoverPhoto(BuildContext context) async { - // final int? coverPhotoID = await showPickCoverPhotoSheet( - // context, - // widget.collection!, - // ); - // if (coverPhotoID != null) { - // unawaited(changeCoverPhoto(context, widget.collection!, coverPhotoID)); - // } + final result = await showPersonAvatarPhotoSheet( + context, + person, + ); + if (result != null) { + _logger.info( + 'Person avatar updated', + ); + setState(() { + person = result; + }); + Bus.instance.fire( + PeopleChangedEvent( + type: PeopleEventType.saveOrEditPerson, + source: "_PeopleAppBarState.setCoverPhoto", + person: result, + ), + ); + } } Future _reassignMe(BuildContext context) async { diff --git a/mobile/lib/ui/viewer/people/people_banner.dart b/mobile/lib/ui/viewer/people/people_banner.dart index 8380853739..5bcffdf837 100644 --- a/mobile/lib/ui/viewer/people/people_banner.dart +++ b/mobile/lib/ui/viewer/people/people_banner.dart @@ -1,7 +1,7 @@ import "package:flutter/material.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/buttons/icon_button_widget.dart"; -import "package:photos/ui/viewer/search/result/person_face_widget.dart"; +import "package:photos/ui/viewer/people/person_face_widget.dart"; enum PeopleBannerType { addName, diff --git a/mobile/lib/ui/viewer/people/person_cluster_suggestion.dart b/mobile/lib/ui/viewer/people/person_cluster_suggestion.dart index 54cfd893d7..220da7d01e 100644 --- a/mobile/lib/ui/viewer/people/person_cluster_suggestion.dart +++ b/mobile/lib/ui/viewer/people/person_cluster_suggestion.dart @@ -18,9 +18,9 @@ import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/buttons/button_widget.dart"; import "package:photos/ui/components/models/button_type.dart"; import "package:photos/ui/viewer/people/cluster_page.dart"; +import "package:photos/ui/viewer/people/file_face_widget.dart"; import "package:photos/ui/viewer/people/person_clusters_page.dart"; -import "package:photos/ui/viewer/search/result/person_face_widget.dart"; -import "package:photos/utils/face/face_box_crop.dart"; +import "package:photos/utils/face/face_thumbnail_cache.dart"; class SuggestionUserFeedback { final bool accepted; @@ -408,11 +408,9 @@ class _PersonClustersState extends State { borderRadius: BorderRadius.circular(75), ), ), - child: PersonFaceWidget( + child: FileFaceWidget( files[start + index], clusterID: cluserId, - useFullFile: true, - thumbnailFallback: false, faceCrop: faceThumbnails[files[start + index].uploadedFileID!], ), diff --git a/mobile/lib/ui/viewer/people/person_clusters_page.dart b/mobile/lib/ui/viewer/people/person_clusters_page.dart index 3c083b2d4b..63b0715303 100644 --- a/mobile/lib/ui/viewer/people/person_clusters_page.dart +++ b/mobile/lib/ui/viewer/people/person_clusters_page.dart @@ -14,7 +14,7 @@ import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/viewer/file/no_thumbnail_widget.dart"; import "package:photos/ui/viewer/people/cluster_page.dart"; -import "package:photos/ui/viewer/search/result/person_face_widget.dart"; +import "package:photos/ui/viewer/people/person_face_widget.dart"; import "package:visibility_detector/visibility_detector.dart"; class PersonClustersPage extends StatefulWidget { @@ -81,7 +81,6 @@ class _PersonClustersPageState extends State { ), child: files.isNotEmpty ? PersonFaceWidget( - files.first, clusterID: clusterID, ) : const NoThumbnailWidget( @@ -283,7 +282,6 @@ class __ClusterWrapperForGirdState extends State<_ClusterWrapperForGird> { ), child: widget.files.isNotEmpty ? PersonFaceWidget( - widget.files.first, clusterID: widget.clusterID, ) : const NoThumbnailWidget( diff --git a/mobile/lib/ui/viewer/people/person_face_widget.dart b/mobile/lib/ui/viewer/people/person_face_widget.dart new file mode 100644 index 0000000000..f69a1d6fda --- /dev/null +++ b/mobile/lib/ui/viewer/people/person_face_widget.dart @@ -0,0 +1,201 @@ +import "dart:typed_data"; + +import 'package:flutter/widgets.dart'; +import "package:logging/logging.dart"; +import "package:photos/db/files_db.dart"; +import "package:photos/db/ml/db.dart"; +import 'package:photos/models/file/file.dart'; +import "package:photos/models/ml/face/face.dart"; +import "package:photos/models/ml/face/person.dart"; +import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; +import "package:photos/services/machine_learning/ml_result.dart"; +import "package:photos/services/search_service.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/common/loading_widget.dart"; +import "package:photos/utils/face/face_thumbnail_cache.dart"; + +final _logger = Logger("PersonFaceWidget"); + +class PersonFaceWidget extends StatefulWidget { + final String? personId; + final String? clusterID; + final bool useFullFile; + final VoidCallback? onErrorCallback; + + // PersonFaceWidget constructor checks that both personId and clusterID are not null + // and that the file is not null + const PersonFaceWidget({ + this.personId, + this.clusterID, + this.useFullFile = true, + this.onErrorCallback, + super.key, + }) : assert( + personId != null || clusterID != null, + "PersonFaceWidget requires either personId or clusterID to be non-null", + ); + + @override + State createState() => _PersonFaceWidgetState(); +} + +class _PersonFaceWidgetState extends State { + Future? faceCropFuture; + EnteFile? fileForFaceCrop; + + bool get isPerson => widget.personId != null; + + @override + void initState() { + super.initState(); + faceCropFuture = _getFaceCrop(); + } + + @override + void dispose() { + super.dispose(); + if (fileForFaceCrop != null) { + checkStopTryingToGenerateFaceThumbnails( + fileForFaceCrop!.uploadedFileID!, + useFullFile: widget.useFullFile, + ); + } + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: faceCropFuture, + builder: (context, snapshot) { + if (snapshot.hasData) { + final ImageProvider imageProvider = MemoryImage(snapshot.data!); + return Stack( + fit: StackFit.expand, + children: [ + Image( + image: imageProvider, + fit: BoxFit.cover, + ), + ], + ); + } else { + if (snapshot.hasError) { + _logger.severe( + "Error getting cover face for person", + snapshot.error, + snapshot.stackTrace, + ); + } + return EnteLoadingWidget( + color: getEnteColorScheme(context).fillMuted, + ); + } + }, + ); + } + + Future _getFaceCrop() async { + try { + final String personOrClusterId = widget.personId ?? widget.clusterID!; + final tryInMemoryCachedCrop = + checkInMemoryCachedCropForPersonOrClusterID(personOrClusterId); + if (tryInMemoryCachedCrop != null) return tryInMemoryCachedCrop; + String? fixedFaceID; + PersonEntity? personEntity; + if (isPerson) { + personEntity = await PersonService.instance.getPerson(widget.personId!); + if (personEntity == null) { + _logger.severe( + "Person with ID ${widget.personId} not found, cannot get cover face.", + ); + return null; + } + fixedFaceID = personEntity.data.avatarFaceID; + } + fixedFaceID ??= + await checkUsedFaceIDForPersonOrClusterId(personOrClusterId); + final hiddenFileIDs = await SearchService.instance + .getHiddenFiles() + .then((onValue) => onValue.map((e) => e.uploadedFileID)); + EnteFile? fileForFaceCrop; + if (fixedFaceID != null) { + final fileID = getFileIdFromFaceId(fixedFaceID); + final fileInDB = await FilesDB.instance.getAnyUploadedFile(fileID); + if (fileInDB == null) { + _logger.severe( + "File with ID $fileID not found in DB, cannot get cover face.", + ); + await checkRemoveCachedFaceIDForPersonOrClusterId( + personOrClusterId, + ); + } else if (hiddenFileIDs.contains(fileInDB.uploadedFileID)) { + _logger.info( + "File with ID $fileID is hidden, skipping it for face crop.", + ); + await checkRemoveCachedFaceIDForPersonOrClusterId( + personOrClusterId, + ); + } else { + fileForFaceCrop = fileInDB; + } + } + if (fileForFaceCrop == null) { + final List allFaces = isPerson + ? await MLDataDB.instance + .getFaceIDsForPersonOrderedByScore(widget.personId!) + : await MLDataDB.instance + .getFaceIDsForClusterOrderedByScore(widget.clusterID!); + for (final faceID in allFaces) { + final fileID = getFileIdFromFaceId(faceID); + if (hiddenFileIDs.contains(fileID)) { + _logger.info( + "File with ID $fileID is hidden, skipping it for face crop.", + ); + continue; + } + fileForFaceCrop = await FilesDB.instance.getAnyUploadedFile(fileID); + if (fileForFaceCrop != null) { + _logger.info( + "Using file ID $fileID for face crop for person: ${widget.personId} or cluster: ${widget.clusterID}", + ); + fixedFaceID = faceID; + break; + } + } + if (fileForFaceCrop == null) { + _logger.severe( + "No suitable file found for face crop for person: ${widget.personId} or cluster: ${widget.clusterID}", + ); + return null; + } + } + final Face? face = await MLDataDB.instance.getCoverFaceForPerson( + recentFileID: fileForFaceCrop.uploadedFileID!, + avatarFaceId: fixedFaceID, + personID: widget.personId, + clusterID: widget.clusterID, + ); + if (face == null) { + debugPrint( + "No cover face for person: ${widget.personId} or cluster ${widget.clusterID} and fileID ${fileForFaceCrop.uploadedFileID!}", + ); + return null; + } + final cropMap = await getCachedFaceCrops( + fileForFaceCrop, + [face], + useFullFile: widget.useFullFile, + personOrClusterID: personOrClusterId, + ); + return cropMap?[face.faceID]; + } catch (e, s) { + _logger.severe( + "Error getting cover face for person: ${widget.personId} or cluster ${widget.clusterID}", + e, + s, + ); + widget.onErrorCallback?.call(); + return null; + } + } +} diff --git a/mobile/lib/ui/viewer/people/person_row_item.dart b/mobile/lib/ui/viewer/people/person_row_item.dart index 10716f4196..36d0f5e272 100644 --- a/mobile/lib/ui/viewer/people/person_row_item.dart +++ b/mobile/lib/ui/viewer/people/person_row_item.dart @@ -1,41 +1,7 @@ import "package:flutter/material.dart"; import "package:photos/models/file/file.dart"; import "package:photos/models/ml/face/person.dart"; -import "package:photos/ui/viewer/search/result/person_face_widget.dart"; - -class PersonRowItem extends StatelessWidget { - final PersonEntity person; - final EnteFile personFile; - final VoidCallback onTap; - - const PersonRowItem({ - super.key, - required this.person, - required this.personFile, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return ListTile( - dense: false, - minLeadingWidth: 0, - contentPadding: const EdgeInsets.symmetric(horizontal: 0), - leading: SizedBox( - width: 56, - height: 56, - child: ClipRRect( - borderRadius: const BorderRadius.all( - Radius.elliptical(16, 12), - ), - child: PersonFaceWidget(personFile, personId: person.remoteID), - ), - ), - title: Text(person.data.name), - onTap: onTap, - ); - } -} +import "package:photos/ui/viewer/people/person_face_widget.dart"; class PersonGridItem extends StatelessWidget { final PersonEntity person; @@ -67,7 +33,7 @@ class PersonGridItem extends StatelessWidget { borderRadius: BorderRadius.circular(80), ), ), - child: PersonFaceWidget(personFile, personId: person.remoteID), + child: PersonFaceWidget(personId: person.remoteID), ), ), const SizedBox(height: 4), diff --git a/mobile/lib/ui/viewer/people/person_selection_action_widgets.dart b/mobile/lib/ui/viewer/people/person_selection_action_widgets.dart index b3a03451c4..26899e037b 100644 --- a/mobile/lib/ui/viewer/people/person_selection_action_widgets.dart +++ b/mobile/lib/ui/viewer/people/person_selection_action_widgets.dart @@ -7,7 +7,6 @@ import "package:photos/core/event_bus.dart"; import "package:photos/events/people_changed_event.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; -import "package:photos/models/file/file.dart"; import "package:photos/models/ml/face/person.dart"; import "package:photos/models/typedefs.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; @@ -17,21 +16,10 @@ import "package:photos/ui/components/buttons/button_widget.dart"; import "package:photos/ui/components/dialog_widget.dart"; import "package:photos/ui/components/models/button_type.dart"; import "package:photos/ui/notification/toast.dart"; -import "package:photos/ui/viewer/file/no_thumbnail_widget.dart"; -import "package:photos/ui/viewer/search/result/person_face_widget.dart"; +import "package:photos/ui/viewer/people/person_face_widget.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/person_contact_linking_util.dart"; -class PersonEntityWithThumbnailFile { - final PersonEntity person; - final EnteFile? thumbnailFile; - - const PersonEntityWithThumbnailFile( - this.person, - this.thumbnailFile, - ); -} - class LinkContactToPersonSelectionPage extends StatefulWidget { final String? emailToLink; const LinkContactToPersonSelectionPage({ @@ -46,25 +34,21 @@ class LinkContactToPersonSelectionPage extends StatefulWidget { class _LinkContactToPersonSelectionPageState extends State { - late Future> - _personEntitiesWithThumnailFile; + late Future> _personEntities; final _logger = Logger('LinkContactToPersonSelectionPage'); @override void initState() { super.initState(); - _personEntitiesWithThumnailFile = - PersonService.instance.getPersons().then((persons) async { - final List result = []; + _personEntities = PersonService.instance.getPersons().then((persons) async { + final List result = []; for (final person in persons) { if ((person.data.email != null && person.data.email!.isNotEmpty) || (person.data.isHidden || person.data.isIgnored)) { continue; } - final file = - await PersonService.instance.getThumbnailFileOfPerson(person); - result.add(PersonEntityWithThumbnailFile(person, file)); + result.add(person); } return result; }); @@ -85,14 +69,14 @@ class _LinkContactToPersonSelectionPageState ), centerTitle: false, ), - body: FutureBuilder>( - future: _personEntitiesWithThumnailFile, + body: FutureBuilder>( + future: _personEntities, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: EnteLoadingWidget()); } else if (snapshot.hasError) { _logger.severe( - "Failed to load _personEntitiesWithThumnailFile", + "Failed to load _personEntities", snapshot.error, snapshot.stackTrace, ); @@ -131,7 +115,7 @@ class _LinkContactToPersonSelectionPageState final updatedPerson = await linkPersonToContact( context, emailToLink: widget.emailToLink!, - personEntity: results[index].person, + personEntity: results[index], ); if (updatedPerson != null) { @@ -143,7 +127,7 @@ class _LinkContactToPersonSelectionPageState } }, itemSize: itemSize, - personEntitiesWithThumbnailFile: results[index], + personEntity: results[index], ); }, ); @@ -221,25 +205,21 @@ class ReassignMeSelectionPage extends StatefulWidget { } class _ReassignMeSelectionPageState extends State { - late Future> - _personEntitiesWithThumnailFile; + late Future> _personEntities; final _logger = Logger('ReassignMeSelectionPage'); @override void initState() { super.initState(); - _personEntitiesWithThumnailFile = - PersonService.instance.getPersons().then((persons) async { - final List result = []; + _personEntities = PersonService.instance.getPersons().then((persons) async { + final List result = []; for (final person in persons) { if ((person.data.email != null && person.data.email!.isNotEmpty) || (person.data.isHidden || person.data.isIgnored)) { continue; } - final file = - await PersonService.instance.getThumbnailFileOfPerson(person); - result.add(PersonEntityWithThumbnailFile(person, file)); + result.add(person); } return result; }); @@ -260,8 +240,8 @@ class _ReassignMeSelectionPageState extends State { ), centerTitle: false, ), - body: FutureBuilder>( - future: _personEntitiesWithThumnailFile, + body: FutureBuilder>( + future: _personEntities, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: EnteLoadingWidget()); @@ -310,12 +290,11 @@ class _ReassignMeSelectionPageState extends State { try { await reassignMe( currentPersonID: widget.currentMeId, - newPersonID: results[index].person.remoteID, + newPersonID: results[index].remoteID, ); showToast( context, - context.l10n - .reassignedToName(results[index].person.data.name), + context.l10n.reassignedToName(results[index].data.name), ); await Future.delayed(const Duration(milliseconds: 1250)); unawaited(dialog.hide()); @@ -328,7 +307,7 @@ class _ReassignMeSelectionPageState extends State { } }, itemSize: itemSize, - personEntitiesWithThumbnailFile: results[index], + personEntity: results[index], ); }, ); @@ -374,12 +353,12 @@ class _ReassignMeSelectionPageState extends State { class _RoundedPersonFaceWidget extends StatelessWidget { final FutureVoidCallback onTap; final double itemSize; - final PersonEntityWithThumbnailFile personEntitiesWithThumbnailFile; + final PersonEntity personEntity; const _RoundedPersonFaceWidget({ required this.onTap, required this.itemSize, - required this.personEntitiesWithThumbnailFile, + required this.personEntity, }); double get borderRadius => 82 * (itemSize / 102); @@ -425,14 +404,10 @@ class _RoundedPersonFaceWidget extends StatelessWidget { ), ), ), - child: personEntitiesWithThumbnailFile.thumbnailFile == null - ? const NoThumbnailWidget(addBorder: false) - : PersonFaceWidget( - personEntitiesWithThumbnailFile.thumbnailFile!, - personId: - personEntitiesWithThumbnailFile.person.remoteID, - useFullFile: true, - ), + child: PersonFaceWidget( + personId: personEntity.remoteID, + useFullFile: true, + ), ), ), ), @@ -441,7 +416,7 @@ class _RoundedPersonFaceWidget extends StatelessWidget { Padding( padding: const EdgeInsets.only(top: 6, bottom: 0), child: Text( - personEntitiesWithThumbnailFile.person.data.name, + personEntity.data.name, maxLines: 1, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, diff --git a/mobile/lib/ui/viewer/people/save_or_edit_person.dart b/mobile/lib/ui/viewer/people/save_or_edit_person.dart index 2d4ddd10cd..8a3f597873 100644 --- a/mobile/lib/ui/viewer/people/save_or_edit_person.dart +++ b/mobile/lib/ui/viewer/people/save_or_edit_person.dart @@ -21,7 +21,6 @@ import "package:photos/models/ml/face/person.dart"; import "package:photos/services/account/user_service.dart"; import "package:photos/services/machine_learning/face_ml/feedback/cluster_feedback.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; -import "package:photos/services/machine_learning/ml_result.dart"; import "package:photos/services/search_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/date_input.dart"; @@ -37,8 +36,8 @@ import "package:photos/ui/viewer/gallery/hooks/pick_person_avatar.dart"; import "package:photos/ui/viewer/people/link_email_screen.dart"; import "package:photos/ui/viewer/people/people_util.dart"; import "package:photos/ui/viewer/people/person_clusters_page.dart"; +import "package:photos/ui/viewer/people/person_face_widget.dart"; import "package:photos/ui/viewer/people/person_row_item.dart"; -import "package:photos/ui/viewer/search/result/person_face_widget.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/navigation_util.dart"; import "package:photos/utils/person_contact_linking_util.dart"; @@ -155,94 +154,72 @@ class _SaveOrEditPersonState extends State { children: [ const SizedBox(height: 48), if (person != null) - FutureBuilder<(String, EnteFile)>( - future: _getRecentFileWithClusterID(person!), - builder: (context, snapshot) { - if (snapshot.hasData) { - final String personClusterID = - snapshot.data!.$1; - final personFile = snapshot.data!.$2; - return Stack( - children: [ - SizedBox( - height: 110, - width: 110, - child: ClipPath( - clipper: ShapeBorderClipper( - shape: ContinuousRectangleBorder( - borderRadius: - BorderRadius.circular(80), - ), - ), - child: snapshot.hasData - ? PersonFaceWidget( - key: ValueKey( - person?.data.avatarFaceID ?? - "", - ), - personFile, - clusterID: personClusterID, - personId: person!.remoteID, - ) - : const NoThumbnailWidget( - addBorder: false, - ), - ), + Stack( + children: [ + SizedBox( + height: 110, + width: 110, + child: ClipPath( + clipper: ShapeBorderClipper( + shape: ContinuousRectangleBorder( + borderRadius: BorderRadius.circular(80), ), - if (person != null) - Positioned( - right: 0, - bottom: 0, - child: Container( - width: 28, - height: 28, - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(8.0), - boxShadow: Theme.of(context) - .colorScheme - .enteTheme - .shadowMenu, - color: getEnteColorScheme(context) - .backgroundElevated2, - ), - child: IconButton( - icon: const Icon(Icons.edit), - iconSize: - 16, // specify the size of the icon - onPressed: () async { - final result = - await showPersonAvatarPhotoSheet( - context, - person!, - ); - if (result != null) { - _logger.info( - 'Person avatar updated', - ); - setState(() { - person = result; - }); - Bus.instance.fire( - PeopleChangedEvent( - type: PeopleEventType - .saveOrEditPerson, - source: - "_SaveOrEditPersonState", - person: result, - ), - ); - } - }, - ), - ), - ), - ], - ); - } else { - return const SizedBox.shrink(); - } - }, + ), + child: PersonFaceWidget( + key: ValueKey( + person?.data.avatarFaceID ?? "", + ), + personId: person!.remoteID, + ), + ), + ), + if (person != null) + Positioned( + right: 0, + bottom: 0, + child: Container( + width: 28, + height: 28, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.0), + boxShadow: Theme.of(context) + .colorScheme + .enteTheme + .shadowMenu, + color: getEnteColorScheme(context) + .backgroundElevated2, + ), + child: IconButton( + icon: const Icon(Icons.edit), + iconSize: + 16, // specify the size of the icon + onPressed: () async { + final result = + await showPersonAvatarPhotoSheet( + context, + person!, + ); + if (result != null) { + _logger.info( + 'Person avatar updated', + ); + setState(() { + person = result; + }); + Bus.instance.fire( + PeopleChangedEvent( + type: PeopleEventType + .saveOrEditPerson, + source: "_SaveOrEditPersonState", + person: result, + ), + ); + } + }, + ), + ), + ), + ], ), if (person == null) SizedBox( @@ -256,7 +233,6 @@ class _SaveOrEditPersonState extends State { ), child: widget.file != null ? PersonFaceWidget( - widget.file!, clusterID: widget.clusterID, ) : const NoThumbnailWidget( @@ -628,10 +604,7 @@ class _SaveOrEditPersonState extends State { _logger.severe( "Failed to addNewPerson, email is already assigned to a person", ); - await showGenericErrorDialog( - context: context, - error: "Email already assigned", - ); + await showAlreadyLinkedEmailDialog(context, email); return null; } @@ -685,7 +658,8 @@ class _SaveOrEditPersonState extends State { _email!.isNotEmpty && _email != person!.data.email && await checkIfEmailAlreadyAssignedToAPerson(_email!)) { - throw Exception("Email already assigned to a person"); + await showAlreadyLinkedEmailDialog(context, _email!); + return null; } final String name = _inputName.trim(); final String? birthDate = _selectedDate; @@ -735,40 +709,6 @@ class _SaveOrEditPersonState extends State { } return personAndFileID; } - - Future<(String, EnteFile)> _getRecentFileWithClusterID( - PersonEntity person, - ) async { - final clustersToFiles = - await SearchService.instance.getClusterFilesForPersonID( - person.remoteID, - ); - int? avatarFileID; - if (person.data.hasAvatar()) { - avatarFileID = tryGetFileIdFromFaceId(person.data.avatarFaceID!); - } - EnteFile? resultFile; - // iterate over all clusters and get the first file - for (final clusterFiles in clustersToFiles.values) { - for (final file in clusterFiles) { - if (avatarFileID != null && file.uploadedFileID! == avatarFileID) { - resultFile = file; - break; - } - resultFile ??= file; - if (resultFile.creationTime! < file.creationTime!) { - resultFile = file; - } - } - } - if (resultFile == null) { - debugPrint( - "Person ${kDebugMode ? person.data.name : person.remoteID} has no files", - ); - return ("", EnteFile()); - } - return (person.remoteID, resultFile); - } } class _EmailSection extends StatefulWidget { @@ -911,13 +851,9 @@ class _EmailSectionState extends State<_EmailSection> { "Error getting isMeAssigned", snapshot.error, ); - return const RepaintBoundary( - child: EnteLoadingWidget(), - ); + return const EnteLoadingWidget(); } else { - return const RepaintBoundary( - child: EnteLoadingWidget(), - ); + return const EnteLoadingWidget(); } }, ), diff --git a/mobile/lib/ui/viewer/search/result/contact_result_page.dart b/mobile/lib/ui/viewer/search/result/contact_result_page.dart index 362437b15a..53b675ed3e 100644 --- a/mobile/lib/ui/viewer/search/result/contact_result_page.dart +++ b/mobile/lib/ui/viewer/search/result/contact_result_page.dart @@ -5,13 +5,19 @@ import 'package:flutter/material.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/events/files_updated_event.dart'; import 'package:photos/events/local_photos_updated_event.dart'; +import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; +import "package:photos/models/collection/collection.dart"; import 'package:photos/models/file/file.dart'; import 'package:photos/models/file_load_result.dart'; import 'package:photos/models/gallery_type.dart'; import "package:photos/models/ml/face/person.dart"; +import "package:photos/models/search/generic_search_result.dart"; +import "package:photos/models/search/search_constants.dart"; import 'package:photos/models/search/search_result.dart'; import 'package:photos/models/selected_files.dart'; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/collections/album/row_item.dart"; import "package:photos/ui/components/end_to_end_banner.dart"; import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; @@ -46,6 +52,7 @@ class ContactResultPage extends StatefulWidget { class _ContactResultPageState extends State { final _selectedFiles = SelectedFiles(); late final List files; + late final List collections; late final StreamSubscription _filesUpdatedEvent; late String _searchResultName; @@ -53,6 +60,8 @@ class _ContactResultPageState extends State { void initState() { super.initState(); files = widget.searchResult.resultFiles(); + collections = (widget.searchResult as GenericSearchResult) + .params[kContactCollections]; _searchResultName = widget.searchResult.name(); _filesUpdatedEvent = Bus.instance.on().listen((event) { @@ -104,8 +113,10 @@ class _ContactResultPageState extends State { initialFiles: widget.searchResult.resultFiles().isNotEmpty ? [widget.searchResult.resultFiles().first] : null, - header: EmailValidator.validate(_searchResultName) - ? Padding( + header: Column( + children: [ + if (EmailValidator.validate(_searchResultName)) + Padding( padding: const EdgeInsets.only(top: 12, bottom: 8), child: EndToEndBanner( title: context.l10n.linkPerson, @@ -125,8 +136,11 @@ class _ContactResultPageState extends State { } }, ), - ) - : null, + ), + if (collections.isNotEmpty) + _AlbumsSection(context: context, collections: collections), + ], + ), ); return GalleryFilesState( @@ -179,3 +193,52 @@ class _ContactResultPageState extends State { ); } } + +class _AlbumsSection extends StatelessWidget { + const _AlbumsSection({ + required this.context, + required this.collections, + }); + + final BuildContext context; + final List collections; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 24, top: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Text( + S.of(context).albums, + style: getEnteTextTheme(context).large, + ), + ), + Align( + alignment: Alignment.centerLeft, + child: SizedBox( + height: 147, + child: ListView.separated( + separatorBuilder: (context, index) => const SizedBox(width: 4), + scrollDirection: Axis.horizontal, + itemCount: collections.length, + padding: const EdgeInsets.symmetric(horizontal: 8), + itemBuilder: (context, index) { + final item = collections[index]; + return AlbumRowItemWidget( + item, + 120, + showFileCount: false, + ); + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/ui/viewer/search/result/people_section_all_page.dart b/mobile/lib/ui/viewer/search/result/people_section_all_page.dart index 544dc4d98a..a24e02d63b 100644 --- a/mobile/lib/ui/viewer/search/result/people_section_all_page.dart +++ b/mobile/lib/ui/viewer/search/result/people_section_all_page.dart @@ -4,42 +4,87 @@ import 'package:flutter/material.dart'; import "package:flutter_animate/flutter_animate.dart"; import "package:photos/events/event.dart"; import "package:photos/generated/l10n.dart"; -import "package:photos/models/search/search_result.dart"; +import "package:photos/models/search/generic_search_result.dart"; +import "package:photos/models/search/hierarchical/face_filter.dart"; import "package:photos/models/search/search_types.dart"; +import "package:photos/models/selected_people.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/viewer/search_tab/people_section.dart"; -class PeopleSectionAllPage extends StatefulWidget { +class PeopleSectionAllPage extends StatelessWidget { const PeopleSectionAllPage({ super.key, }); @override - State createState() => _PeopleSectionAllPageState(); + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(SectionType.face.sectionTitle(context)), + centerTitle: false, + ), + body: const PeopleSectionAllWidget(), + ); + } } -class _PeopleSectionAllPageState extends State { - late Future> sectionData; +class PeopleSectionAllWidget extends StatefulWidget { + const PeopleSectionAllWidget({ + super.key, + this.selectedPeople, + this.namedOnly = false, + }); + + final SelectedPeople? selectedPeople; + final bool namedOnly; + + @override + State createState() => _PeopleSectionAllWidgetState(); +} + +class _PeopleSectionAllWidgetState extends State { + late Future> sectionData; final streamSubscriptions = []; @override void initState() { super.initState(); - sectionData = SectionType.face.getData(context); + sectionData = getResults(); final streamsToListenTo = SectionType.face.viewAllUpdateEvents(); for (Stream stream in streamsToListenTo) { streamSubscriptions.add( stream.listen((event) async { setState(() { - sectionData = SectionType.face.getData(context); + sectionData = getResults(); }); }), ); } } + Future> getResults() async { + final results = + List.from(await SectionType.face.getData(context)); + + if (widget.namedOnly) { + results.removeWhere( + (element) => + (element.hierarchicalSearchFilter as FaceFilter).personId == null, + ); + if (widget.selectedPeople?.personIds.isEmpty ?? false) { + widget.selectedPeople!.select( + results + .take(2) + .map((e) => (e.hierarchicalSearchFilter as FaceFilter).personId!) + .toSet(), + ); + } + } + return results; + } + @override void dispose() { for (var subscriptions in streamSubscriptions) { @@ -55,67 +100,64 @@ class _PeopleSectionAllPageState extends State { MediaQuery.textScalerOf(context).scale(smallFontSize) / smallFontSize; const horizontalEdgePadding = 20.0; const gridPadding = 16.0; - return Scaffold( - appBar: AppBar( - title: Text(SectionType.face.sectionTitle(context)), - centerTitle: false, - ), - body: FutureBuilder>( - future: sectionData, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: EnteLoadingWidget()); - } else if (snapshot.hasError) { - return const Center(child: Icon(Icons.error_outline_rounded)); - } else if (!snapshot.hasData || snapshot.data!.isEmpty) { - return Center(child: Text(S.of(context).noResultsFound + '.')); - } else { - final results = snapshot.data!; - final screenWidth = MediaQuery.of(context).size.width; - final crossAxisCount = (screenWidth / 100).floor(); - final itemSize = (screenWidth - - ((horizontalEdgePadding * 2) + - ((crossAxisCount - 1) * gridPadding))) / - crossAxisCount; + return FutureBuilder>( + future: sectionData, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: EnteLoadingWidget()); + } else if (snapshot.hasError) { + return const Center(child: Icon(Icons.error_outline_rounded)); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return Center(child: Text(S.of(context).noResultsFound + '.')); + } else { + final results = snapshot.data!; + final screenWidth = MediaQuery.of(context).size.width; + final crossAxisCount = (screenWidth / 100).floor(); - return GridView.builder( - padding: const EdgeInsets.fromLTRB( - horizontalEdgePadding, - 16, - horizontalEdgePadding, - 96, - ), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - mainAxisSpacing: gridPadding, - crossAxisSpacing: gridPadding, - crossAxisCount: crossAxisCount, - childAspectRatio: - itemSize / (itemSize + (24 * textScaleFactor)), - ), - itemCount: results.length, - itemBuilder: (context, index) { - return PersonSearchExample( - searchResult: results[index], - size: itemSize, - ) - .animate(delay: Duration(milliseconds: index * 13)) - .fadeIn( - duration: const Duration(milliseconds: 225), - curve: Curves.easeIn, - ) - .slide( - begin: const Offset(0, -0.06), - curve: Curves.easeInOut, - duration: const Duration( - milliseconds: 225, - ), - ); - }, - ); - } - }, - ), + final itemSize = (screenWidth - + ((horizontalEdgePadding * 2) + + ((crossAxisCount - 1) * gridPadding))) / + crossAxisCount; + + return GridView.builder( + padding: const EdgeInsets.fromLTRB( + horizontalEdgePadding, + 16, + horizontalEdgePadding, + 96, + ), + shrinkWrap: true, + primary: false, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + mainAxisSpacing: gridPadding, + crossAxisSpacing: gridPadding, + crossAxisCount: crossAxisCount, + childAspectRatio: itemSize / (itemSize + (24 * textScaleFactor)), + ), + itemCount: results.length, + itemBuilder: (context, index) { + return PersonSearchExample( + searchResult: results[index], + size: itemSize, + selectedPeople: widget.selectedPeople, + ) + .animate(delay: Duration(milliseconds: index * 13)) + .fadeIn( + duration: const Duration(milliseconds: 225), + curve: Curves.easeIn, + ) + .slide( + begin: const Offset(0, -0.06), + curve: Curves.easeInOut, + duration: const Duration( + milliseconds: 225, + ), + ); + }, + ); + } + }, ); } } diff --git a/mobile/lib/ui/viewer/search/result/person_face_widget.dart b/mobile/lib/ui/viewer/search/result/person_face_widget.dart deleted file mode 100644 index 62c18ec618..0000000000 --- a/mobile/lib/ui/viewer/search/result/person_face_widget.dart +++ /dev/null @@ -1,187 +0,0 @@ -import "dart:typed_data"; - -import 'package:flutter/widgets.dart'; -import "package:logging/logging.dart"; -import "package:photos/db/files_db.dart"; -import "package:photos/db/ml/db.dart"; -import 'package:photos/models/file/file.dart'; -import "package:photos/models/ml/face/face.dart"; -import "package:photos/models/ml/face/person.dart"; -import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; -import "package:photos/services/machine_learning/ml_result.dart"; -import "package:photos/services/search_service.dart"; -import "package:photos/theme/ente_theme.dart"; -import "package:photos/ui/common/loading_widget.dart"; -import "package:photos/ui/viewer/file/thumbnail_widget.dart"; -import "package:photos/utils/face/face_box_crop.dart"; - -final _logger = Logger("PersonFaceWidget"); - -class PersonFaceWidget extends StatefulWidget { - final EnteFile file; - final String? personId; - final String? clusterID; - final bool useFullFile; - final bool thumbnailFallback; - final bool cannotTrustFile; - final Uint8List? faceCrop; - final VoidCallback? onErrorCallback; - - // PersonFaceWidget constructor checks that both personId and clusterID are not null - // and that the file is not null - const PersonFaceWidget( - this.file, { - this.personId, - this.clusterID, - this.useFullFile = true, - this.thumbnailFallback = false, - this.cannotTrustFile = false, - this.faceCrop, - this.onErrorCallback, - super.key, - }) : assert( - personId != null || clusterID != null, - "PersonFaceWidget requires either personId or clusterID to be non-null", - ); - - @override - State createState() => _PersonFaceWidgetState(); -} - -class _PersonFaceWidgetState extends State { - Future? faceCropFuture; - late final mlDataDB = MLDataDB.instance; - - @override - void initState() { - super.initState(); - faceCropFuture = widget.faceCrop != null - ? Future.value(widget.faceCrop) - : _getFaceCrop(); - } - - @override - void dispose() { - super.dispose(); - checkStopTryingToGenerateFaceThumbnails( - widget.file, - useFullFile: widget.useFullFile, - ); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: faceCropFuture, - builder: (context, snapshot) { - if (snapshot.hasData) { - final ImageProvider imageProvider = MemoryImage(snapshot.data!); - return Stack( - fit: StackFit.expand, - children: [ - Image( - image: imageProvider, - fit: BoxFit.cover, - ), - ], - ); - } else { - if (snapshot.hasError) { - _logger.severe( - "Error getting cover face for person: ${snapshot.error} ${snapshot.stackTrace}}", - ); - } - return widget.thumbnailFallback - ? ThumbnailWidget(widget.file) - : EnteLoadingWidget( - color: getEnteColorScheme(context).fillMuted, - ); - } - }, - ); - } - - Future _getFaceCrop() async { - try { - EnteFile? fileForFaceCrop = widget.file; - String? personAvatarFaceID; - Iterable? allFaces; - if (widget.personId != null) { - final PersonEntity? personEntity = - await PersonService.instance.getPerson(widget.personId!); - if (personEntity != null) { - personAvatarFaceID = personEntity.data.avatarFaceID; - if (personAvatarFaceID != null) { - final tryCache = - await checkGetCachedCropForFaceID(personAvatarFaceID); - if (tryCache != null) return tryCache; - } - if (personAvatarFaceID == null && widget.cannotTrustFile) { - allFaces = await mlDataDB.getFaceIDsForPerson(widget.personId!); - } - } - } else if (widget.clusterID != null && widget.cannotTrustFile) { - allFaces = await mlDataDB.getFaceIDsForCluster(widget.clusterID!); - } - if (allFaces != null) { - final allFileIDs = - allFaces.map((e) => getFileIdFromFaceId(e)).toSet(); - final hiddenFileIDs = await SearchService.instance - .getHiddenFiles() - .then((onValue) => onValue.map((e) => e.uploadedFileID)); - final acceptableFileIDs = allFileIDs.difference(hiddenFileIDs.toSet()); - final fileIDToCreationTime = - await FilesDB.instance.getFileIDToCreationTime(); - // Get the file with the most recent creation time - final recentFileID = acceptableFileIDs.reduce((a, b) { - final aTime = fileIDToCreationTime[a]; - final bTime = fileIDToCreationTime[b]; - if (aTime == null) { - return b; - } - if (bTime == null) { - return a; - } - return (aTime >= bTime) ? a : b; - }); - if (fileForFaceCrop.uploadedFileID != recentFileID) { - fileForFaceCrop = - await FilesDB.instance.getAnyUploadedFile(recentFileID); - if (fileForFaceCrop == null) return null; - } - } - - final Face? face = await mlDataDB.getCoverFaceForPerson( - recentFileID: fileForFaceCrop.uploadedFileID!, - avatarFaceId: personAvatarFaceID, - personID: widget.personId, - clusterID: widget.clusterID, - ); - if (face == null) { - debugPrint( - "No cover face for person: ${widget.personId} and cluster ${widget.clusterID} and recentFile ${widget.file.uploadedFileID}", - ); - return null; - } - if (face.fileID != fileForFaceCrop.uploadedFileID!) { - fileForFaceCrop = - await FilesDB.instance.getAnyUploadedFile(face.fileID); - if (fileForFaceCrop == null) return null; - } - final cropMap = await getCachedFaceCrops( - fileForFaceCrop, - [face], - useFullFile: widget.useFullFile, - ); - return cropMap?[face.faceID]; - } catch (e, s) { - _logger.severe( - "Error getting cover face for person: ${widget.personId} and cluster ${widget.clusterID}", - e, - s, - ); - widget.onErrorCallback?.call(); - return null; - } - } -} diff --git a/mobile/lib/ui/viewer/search/result/search_section_all_page.dart b/mobile/lib/ui/viewer/search/result/search_section_all_page.dart index d5de651e69..c4dfed36a7 100644 --- a/mobile/lib/ui/viewer/search/result/search_section_all_page.dart +++ b/mobile/lib/ui/viewer/search/result/search_section_all_page.dart @@ -120,13 +120,6 @@ class _SearchSectionAllPageState extends State { ); } - if (widget.sectionType == SectionType.contacts) { - final split = sectionResults.splitMatch( - (e) => e.resultFiles().isNotEmpty, - ); - sectionResults = split.matched + split.unmatched; - } - if (widget.sectionType == SectionType.location) { final result = sectionResults.splitMatch( (e) => e.type() == ResultType.location, diff --git a/mobile/lib/ui/viewer/search/result/search_thumbnail_widget.dart b/mobile/lib/ui/viewer/search/result/search_thumbnail_widget.dart index aa0b6dc6d0..3d26c1b9ae 100644 --- a/mobile/lib/ui/viewer/search/result/search_thumbnail_widget.dart +++ b/mobile/lib/ui/viewer/search/result/search_thumbnail_widget.dart @@ -6,13 +6,11 @@ import "package:photos/models/search/generic_search_result.dart"; import "package:photos/models/search/search_constants.dart"; import "package:photos/models/search/search_result.dart"; import "package:photos/models/search/search_types.dart"; -import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import "package:photos/theme/ente_theme.dart"; -import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/sharing/user_avator_widget.dart"; import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart'; import 'package:photos/ui/viewer/file/thumbnail_widget.dart'; -import 'package:photos/ui/viewer/search/result/person_face_widget.dart'; +import 'package:photos/ui/viewer/people/person_face_widget.dart'; class SearchThumbnailWidget extends StatelessWidget { final EnteFile? file; @@ -39,7 +37,6 @@ class SearchThumbnailWidget extends StatelessWidget { ? (searchResult != null && searchResult!.type() == ResultType.faces) ? PersonFaceWidget( - file!, personId: (searchResult as GenericSearchResult) .params[kPersonParamID], clusterID: (searchResult as GenericSearchResult) @@ -73,7 +70,7 @@ class ContactSearchThumbnailWidget extends StatefulWidget { class _ContactSearchThumbnailWidgetState extends State { - Future? _mostRecentFileOfPerson; + bool _canUsePersonFaceWidget = true; late String? _personID; late String _email; final _logger = Logger("_ContactSearchThumbnailWidgetState"); @@ -83,16 +80,7 @@ class _ContactSearchThumbnailWidgetState super.initState(); _personID = widget.searchResult.params[kPersonParamID]; _email = widget.searchResult.params[kContactEmail]; - if (_personID != null) { - _mostRecentFileOfPerson = - PersonService.instance.getPerson(_personID!).then((person) { - if (person == null) { - return null; - } else { - return PersonService.instance.getThumbnailFileOfPerson(person); - } - }); - } + _canUsePersonFaceWidget = _personID != null; } @override @@ -102,37 +90,17 @@ class _ContactSearchThumbnailWidgetState width: 60, child: ClipRRect( borderRadius: const BorderRadius.horizontal(left: Radius.circular(4)), - child: _mostRecentFileOfPerson != null - ? FutureBuilder( - future: _mostRecentFileOfPerson, - builder: (context, snapshot) { - if (snapshot.hasData) { - return PersonFaceWidget( - snapshot.data!, - personId: _personID, - onErrorCallback: () { - if (mounted) { - setState(() { - _mostRecentFileOfPerson = null; - }); - } - }, - ); - } else if (snapshot.connectionState == ConnectionState.done && - snapshot.data == null) { - return NoFaceForContactWidget( - user: User(email: _email), - ); - } else if (snapshot.hasError) { + child: _canUsePersonFaceWidget + ? PersonFaceWidget( + personId: _personID, + onErrorCallback: () { + if (mounted) { _logger.severe( - "Error loading personID", - snapshot.error, + "Failed to load face for person with ID: $_personID", ); - return NoFaceForContactWidget( - user: User(email: _email), - ); - } else { - return const EnteLoadingWidget(); + setState(() { + _canUsePersonFaceWidget = false; + }); } }, ) diff --git a/mobile/lib/ui/viewer/search/result/searchable_item.dart b/mobile/lib/ui/viewer/search/result/searchable_item.dart index 90c457f77c..a78d1292f1 100644 --- a/mobile/lib/ui/viewer/search/result/searchable_item.dart +++ b/mobile/lib/ui/viewer/search/result/searchable_item.dart @@ -133,11 +133,12 @@ class SearchableItemWidget extends StatelessWidget { ], ), ), - const Flexible( + Flexible( flex: 1, child: IconButtonWidget( icon: Icons.chevron_right_outlined, iconButtonType: IconButtonType.secondary, + iconColor: colorScheme.blurStrokePressed, ), ), ], diff --git a/mobile/lib/ui/viewer/search_tab/contacts_section.dart b/mobile/lib/ui/viewer/search_tab/contacts_section.dart index bc751fbfa4..ecf5dd7375 100644 --- a/mobile/lib/ui/viewer/search_tab/contacts_section.dart +++ b/mobile/lib/ui/viewer/search_tab/contacts_section.dart @@ -7,20 +7,18 @@ import "package:photos/core/constants.dart"; import "package:photos/events/event.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/models/api/collection/user.dart"; -import "package:photos/models/file/file.dart"; import "package:photos/models/search/generic_search_result.dart"; import "package:photos/models/search/recent_searches.dart"; import "package:photos/models/search/search_constants.dart"; import "package:photos/models/search/search_types.dart"; -import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import "package:photos/theme/ente_theme.dart"; -import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/sharing/user_avator_widget.dart"; +import "package:photos/ui/viewer/people/person_face_widget.dart"; import "package:photos/ui/viewer/search/result/contact_result_page.dart"; -import "package:photos/ui/viewer/search/result/person_face_widget.dart"; import "package:photos/ui/viewer/search/search_section_cta.dart"; import "package:photos/ui/viewer/search_tab/section_header.dart"; import "package:photos/utils/navigation_util.dart"; +import "package:photos/utils/standalone/debouncer.dart"; class ContactsSection extends StatefulWidget { final List contactSearchResults; @@ -33,6 +31,9 @@ class ContactsSection extends StatefulWidget { class _ContactsSectionState extends State { late List _contactSearchResults; final streamSubscriptions = []; + final _debouncer = Debouncer( + const Duration(milliseconds: 1500), + ); @override void initState() { @@ -43,11 +44,13 @@ class _ContactsSectionState extends State { for (Stream stream in streamsToListenTo) { streamSubscriptions.add( stream.listen((event) async { - _contactSearchResults = (await SectionType.contacts.getData( - context, - limit: kSearchSectionLimit, - )) as List; - setState(() {}); + _debouncer.run(() async { + _contactSearchResults = (await SectionType.contacts.getData( + context, + limit: kSearchSectionLimit, + )) as List; + setState(() {}); + }); }), ); } @@ -58,6 +61,7 @@ class _ContactsSectionState extends State { for (var subscriptions in streamSubscriptions) { subscriptions.cancel(); } + _debouncer.cancelDebounceTimer(); super.dispose(); } @@ -149,7 +153,7 @@ class ContactRecommendation extends StatefulWidget { } class _ContactRecommendationState extends State { - Future? _mostRecentFileOfPerson; + bool _canUsePersonFaceWidget = true; late String? _personID; final _logger = Logger("_ContactRecommendationState"); @@ -157,16 +161,7 @@ class _ContactRecommendationState extends State { void initState() { super.initState(); _personID = widget.contactSearchResult.params[kPersonParamID]; - if (_personID != null) { - _mostRecentFileOfPerson = - PersonService.instance.getPerson(_personID!).then((person) { - if (person == null) { - return null; - } else { - return PersonService.instance.getThumbnailFileOfPerson(person); - } - }); - } + _canUsePersonFaceWidget = _personID != null; } @override @@ -203,44 +198,17 @@ class _ContactRecommendationState extends State { child: SizedBox( width: 67.75, height: 67.75, - child: _mostRecentFileOfPerson != null - ? FutureBuilder( - future: _mostRecentFileOfPerson, - builder: (context, snapshot) { - if (snapshot.hasData) { - return PersonFaceWidget( - snapshot.data!, - personId: _personID, - onErrorCallback: () { - if (mounted) { - setState(() { - _mostRecentFileOfPerson = null; - }); - } - }, - ); - } else if (snapshot.connectionState == - ConnectionState.done && - snapshot.data == null) { - return FirstLetterUserAvatar( - User( - email: widget.contactSearchResult - .params[kContactEmail], - ), - ); - } else if (snapshot.hasError) { + child: _canUsePersonFaceWidget + ? PersonFaceWidget( + personId: _personID, + onErrorCallback: () { + if (mounted) { _logger.severe( - "Error loading personID", - snapshot.error, + "Failed to load face for person with ID: $_personID", ); - return FirstLetterUserAvatar( - User( - email: widget.contactSearchResult - .params[kContactEmail], - ), - ); - } else { - return const EnteLoadingWidget(); + setState(() { + _canUsePersonFaceWidget = false; + }); } }, ) diff --git a/mobile/lib/ui/viewer/search_tab/people_section.dart b/mobile/lib/ui/viewer/search_tab/people_section.dart index 644c0966b9..48b0506609 100644 --- a/mobile/lib/ui/viewer/search_tab/people_section.dart +++ b/mobile/lib/ui/viewer/search_tab/people_section.dart @@ -6,25 +6,27 @@ import "package:photos/events/event.dart"; import "package:photos/models/file/file.dart"; import "package:photos/models/ml/face/person.dart"; import "package:photos/models/search/generic_search_result.dart"; +import "package:photos/models/search/hierarchical/face_filter.dart"; import "package:photos/models/search/recent_searches.dart"; import "package:photos/models/search/search_constants.dart"; import "package:photos/models/search/search_result.dart"; import "package:photos/models/search/search_types.dart"; +import "package:photos/models/selected_people.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/settings/ml/machine_learning_settings_page.dart"; import "package:photos/ui/viewer/file/no_thumbnail_widget.dart"; import "package:photos/ui/viewer/file/thumbnail_widget.dart"; import "package:photos/ui/viewer/people/add_person_action_sheet.dart"; import "package:photos/ui/viewer/people/people_page.dart"; +import 'package:photos/ui/viewer/people/person_face_widget.dart'; import "package:photos/ui/viewer/search/result/people_section_all_page.dart"; -import 'package:photos/ui/viewer/search/result/person_face_widget.dart'; import "package:photos/ui/viewer/search/result/search_result_page.dart"; import "package:photos/ui/viewer/search/search_section_cta.dart"; import "package:photos/utils/navigation_util.dart"; class PeopleSection extends StatefulWidget { final SectionType sectionType = SectionType.face; - final List examples; + final List examples; final int limit; const PeopleSection({ @@ -38,7 +40,7 @@ class PeopleSection extends StatefulWidget { } class _PeopleSectionState extends State { - late List _examples; + late List _examples; final streamSubscriptions = []; @override @@ -50,10 +52,12 @@ class _PeopleSectionState extends State { for (Stream stream in streamsToListenTo) { streamSubscriptions.add( stream.listen((event) async { - _examples = await widget.sectionType.getData( - context, - limit: kSearchSectionLimit, - ); + _examples = await widget.sectionType + .getData( + context, + limit: kSearchSectionLimit, + ) + .then((value) => List.from(value)); setState(() {}); }), ); @@ -161,7 +165,7 @@ class _PeopleSectionState extends State { class SearchExampleRow extends StatelessWidget { final SectionType sectionType; - final List examples; + final List examples; const SearchExampleRow(this.examples, this.sectionType, {super.key}); @@ -177,6 +181,7 @@ class SearchExampleRow extends StatelessWidget { itemBuilder: (context, index) { return PersonSearchExample( searchResult: examples[index], + selectedPeople: null, ); }, separatorBuilder: (context, index) => const SizedBox(width: 3), @@ -186,15 +191,23 @@ class SearchExampleRow extends StatelessWidget { } class PersonSearchExample extends StatelessWidget { - final SearchResult searchResult; + final GenericSearchResult searchResult; final double size; + final SelectedPeople? selectedPeople; const PersonSearchExample({ super.key, required this.searchResult, + required this.selectedPeople, this.size = 102, }); + void toggleSelection() { + selectedPeople?.toggleSelection( + (searchResult.hierarchicalSearchFilter as FaceFilter).personId!, + ); + } + @override Widget build(BuildContext context) { final borderRadius = 82 * (size / 102); @@ -202,124 +215,163 @@ class PersonSearchExample extends StatelessWidget { final bool isCluster = (searchResult.type() == ResultType.faces && int.tryParse(searchResult.name()) != null); - return GestureDetector( - onTap: () { - RecentSearches().add(searchResult.name()); - final genericSearchResult = searchResult as GenericSearchResult; - if (genericSearchResult.onResultTap != null) { - genericSearchResult.onResultTap!(context); - } else { - routeToPage( - context, - SearchResultPage(searchResult), - ); - } - }, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, + return ListenableBuilder( + listenable: selectedPeople ?? ValueNotifier(false), + builder: (context, _) { + final filter = (searchResult.hierarchicalSearchFilter as FaceFilter); + final id = filter.personId ?? filter.clusterId ?? ""; + final bool isSelected = selectedPeople?.isPersonSelected(id) ?? false; + + return GestureDetector( + onTap: selectedPeople != null + ? toggleSelection + : () { + RecentSearches().add(searchResult.name()); + if (searchResult.onResultTap != null) { + searchResult.onResultTap!(context); + } else { + routeToPage( + context, + SearchResultPage(searchResult), + ); + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ - ClipPath( - clipper: ShapeBorderClipper( - shape: ContinuousRectangleBorder( - borderRadius: BorderRadius.circular(borderRadius), + Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + ClipPath( + clipper: ShapeBorderClipper( + shape: ContinuousRectangleBorder( + borderRadius: BorderRadius.circular(borderRadius), + ), + ), + child: Container( + width: size, + height: size, + decoration: BoxDecoration( + color: getEnteColorScheme(context).strokeFaint, + ), + ), ), - ), - child: Container( - width: size, - height: size, - decoration: BoxDecoration( - color: getEnteColorScheme(context).strokeFaint, - ), - ), - ), - SizedBox( - width: size - 2, - height: size - 2, - child: searchResult.previewThumbnail() != null - ? ClipPath( - clipper: ShapeBorderClipper( - shape: ContinuousRectangleBorder( - borderRadius: - BorderRadius.circular(borderRadius - 1), - ), - ), - child: searchResult.type() != ResultType.faces + Builder( + builder: (context) { + late Widget child; + + if (searchResult.previewThumbnail() != null) { + child = searchResult.type() != ResultType.faces ? ThumbnailWidget( searchResult.previewThumbnail()!, shouldShowSyncStatus: false, ) - : FaceSearchResult(searchResult), - ) - : ClipPath( - clipper: ShapeBorderClipper( - shape: ContinuousRectangleBorder( - borderRadius: BorderRadius.circular(81), + : FaceSearchResult(searchResult); + } else { + child = const NoThumbnailWidget( + addBorder: false, + ); + } + return SizedBox( + width: size - 2, + height: size - 2, + child: ClipPath( + clipper: ShapeBorderClipper( + shape: ContinuousRectangleBorder( + borderRadius: + searchResult.previewThumbnail() != null + ? BorderRadius.circular(borderRadius - 1) + : BorderRadius.circular(81), + ), + ), + child: ColorFiltered( + colorFilter: ColorFilter.mode( + Colors.black.withOpacity( + isSelected ? 0.4 : 0, + ), + BlendMode.darken, + ), + child: child, ), ), - child: const NoThumbnailWidget( - addBorder: false, - ), - ), - ), - ], - ), - isCluster - ? GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () async { - final result = await showAssignPersonAction( - context, - clusterID: searchResult.name(), - ); - if (result != null && result is (PersonEntity, EnteFile)) { - // ignore: unawaited_futures - routeToPage( - context, - PeoplePage( - person: result.$1, - searchResult: null, - ), ); - } else if (result != null && result is PersonEntity) { - // ignore: unawaited_futures - routeToPage( - context, - PeoplePage( - person: result, - searchResult: null, - ), - ); - } - }, - child: Padding( - padding: const EdgeInsets.only(top: 6, bottom: 0), - child: Text( - "Add name", - maxLines: 1, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: getEnteTextTheme(context).small, + }, + ), + Positioned( + top: 5, + right: 5, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + child: isSelected + ? const Icon( + Icons.check_circle_rounded, + color: Colors.white, + size: 22, + ) + : null, ), ), - ) - : Padding( - padding: const EdgeInsets.only(top: 6, bottom: 0), - child: Text( - searchResult.name(), - maxLines: 1, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: getEnteTextTheme(context).small, - ), - ), - ], - ), + ], + ), + isCluster + ? GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () async { + final result = await showAssignPersonAction( + context, + clusterID: searchResult.name(), + ); + if (result != null && + result is (PersonEntity, EnteFile)) { + // ignore: unawaited_futures + routeToPage( + context, + PeoplePage( + person: result.$1, + searchResult: null, + ), + ); + } else if (result != null && result is PersonEntity) { + // ignore: unawaited_futures + routeToPage( + context, + PeoplePage( + person: result, + searchResult: null, + ), + ); + } + }, + child: Padding( + padding: const EdgeInsets.only(top: 6, bottom: 0), + child: Text( + "Add name", + maxLines: 1, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: getEnteTextTheme(context).small, + ), + ), + ) + : Padding( + padding: const EdgeInsets.only(top: 6, bottom: 0), + child: Text( + searchResult.name(), + maxLines: 1, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: getEnteTextTheme(context).small, + ), + ), + ], + ), + ); + }, ); } } @@ -333,7 +385,6 @@ class FaceSearchResult extends StatelessWidget { Widget build(BuildContext context) { final params = (searchResult as GenericSearchResult).params; return PersonFaceWidget( - searchResult.previewThumbnail()!, personId: params[kPersonParamID], clusterID: params[kClusterParamId], key: params.containsKey(kPersonWidgetKey) diff --git a/mobile/lib/ui/viewer/search_tab/search_tab.dart b/mobile/lib/ui/viewer/search_tab/search_tab.dart index ec616f7ec4..64643232c3 100644 --- a/mobile/lib/ui/viewer/search_tab/search_tab.dart +++ b/mobile/lib/ui/viewer/search_tab/search_tab.dart @@ -16,7 +16,6 @@ import "package:photos/ui/viewer/search/result/no_result_widget.dart"; import "package:photos/ui/viewer/search/search_suggestions.dart"; import "package:photos/ui/viewer/search/tab_empty_state.dart"; import 'package:photos/ui/viewer/search_tab/albums_section.dart'; -import "package:photos/ui/viewer/search_tab/contacts_section.dart"; import "package:photos/ui/viewer/search_tab/file_type_section.dart"; import "package:photos/ui/viewer/search_tab/locations_section.dart"; import "package:photos/ui/viewer/search_tab/magic_section.dart"; @@ -142,10 +141,7 @@ class _AllSearchSectionsState extends State { as List, ); case SectionType.contacts: - return ContactsSection( - snapshot.data!.elementAt(index) - as List, - ); + return const SizedBox.shrink(); case SectionType.fileTypesAndExtension: return FileTypeSection( snapshot.data!.elementAt(index) diff --git a/mobile/lib/ui/viewer/search_tab/section_header.dart b/mobile/lib/ui/viewer/search_tab/section_header.dart index bd97c7215f..31f1f4a4d2 100644 --- a/mobile/lib/ui/viewer/search_tab/section_header.dart +++ b/mobile/lib/ui/viewer/search_tab/section_header.dart @@ -39,7 +39,7 @@ class SectionHeader extends StatelessWidget { padding: const EdgeInsets.fromLTRB(24, 12, 12, 12), child: Icon( Icons.chevron_right_outlined, - color: getEnteColorScheme(context).strokeMuted, + color: getEnteColorScheme(context).blurStrokePressed, ), ), ) diff --git a/mobile/lib/utils/face/face_box_crop.dart b/mobile/lib/utils/face/face_thumbnail_cache.dart similarity index 76% rename from mobile/lib/utils/face/face_box_crop.dart rename to mobile/lib/utils/face/face_thumbnail_cache.dart index ced0c84d08..37542dd1f8 100644 --- a/mobile/lib/utils/face/face_box_crop.dart +++ b/mobile/lib/utils/face/face_thumbnail_cache.dart @@ -22,6 +22,9 @@ final _logger = Logger("FaceCropUtils"); const int _retryLimit = 3; final LRUMap _faceCropCache = LRUMap(1000); final LRUMap _faceCropThumbnailCache = LRUMap(1000); + +final LRUMap _personOrClusterIdToCachedFaceID = LRUMap(1000); + TaskQueue _queueFullFileFaceGenerations = TaskQueue( maxConcurrentTasks: 5, taskTimeout: const Duration(minutes: 1), @@ -33,36 +36,90 @@ TaskQueue _queueThumbnailFaceGenerations = TaskQueue( maxQueueSize: 100, ); -Future checkGetCachedCropForFaceID(String faceID) async { +Uint8List? checkInMemoryCachedCropForPersonOrClusterID( + String personOrClusterID, +) { + final String? faceID = + _personOrClusterIdToCachedFaceID.get(personOrClusterID); + if (faceID == null) return null; final Uint8List? cachedCover = _faceCropCache.get(faceID); return cachedCover; } -Future putCachedCropForFaceID( - String faceID, - Uint8List data, -) async { - _faceCropCache.put(faceID, data); +Uint8List? _checkInMemoryCachedCropForFaceID(String faceID) { + final Uint8List? cachedCover = _faceCropCache.get(faceID); + return cachedCover; } +Future checkUsedFaceIDForPersonOrClusterId( + String personOrClusterID, +) async { + final String? cachedFaceID = + _personOrClusterIdToCachedFaceID.get(personOrClusterID); + if (cachedFaceID != null) return cachedFaceID; + final String? faceIDFromDB = await MLDataDB.instance + .getFaceIdUsedForPersonOrCluster(personOrClusterID); + if (faceIDFromDB != null) { + _personOrClusterIdToCachedFaceID.put(personOrClusterID, faceIDFromDB); + } + return faceIDFromDB; +} + +Future putFaceIdCachedForPersonOrCluster( + String personOrClusterID, + String faceID, +) async { + await MLDataDB.instance.putFaceIdCachedForPersonOrCluster( + personOrClusterID, + faceID, + ); + _personOrClusterIdToCachedFaceID.put(personOrClusterID, faceID); +} + +Future _putCachedCropForFaceID( + String faceID, + Uint8List data, [ + String? personOrClusterID, +]) async { + _faceCropCache.put(faceID, data); + if (personOrClusterID != null) { + await putFaceIdCachedForPersonOrCluster(personOrClusterID, faceID); + } +} + +Future checkRemoveCachedFaceIDForPersonOrClusterId( + String personOrClusterID, +) async { + final String? cachedFaceID = await MLDataDB.instance + .getFaceIdUsedForPersonOrCluster(personOrClusterID); + if (cachedFaceID != null) { + _personOrClusterIdToCachedFaceID.remove(personOrClusterID); + await MLDataDB.instance + .removeFaceIdCachedForPersonOrCluster(personOrClusterID); + } +} + +/// Careful to only use [personOrClusterID] if all [faces] are from the same person or cluster. Future?> getCachedFaceCrops( EnteFile enteFile, Iterable faces, { int fetchAttempt = 1, bool useFullFile = true, + String? personOrClusterID, }) async { try { final faceIdToCrop = {}; final facesWithoutCrops = {}; for (final face in faces) { - final Uint8List? cachedFace = _faceCropCache.get(face.faceID); + final Uint8List? cachedFace = + _checkInMemoryCachedCropForFaceID(face.faceID); if (cachedFace != null) { faceIdToCrop[face.faceID] = cachedFace; } else { final faceCropCacheFile = cachedFaceCropPath(face.faceID); if ((await faceCropCacheFile.exists())) { final data = await faceCropCacheFile.readAsBytes(); - _faceCropCache.put(face.faceID, data); + await _putCachedCropForFaceID(face.faceID, data, personOrClusterID); faceIdToCrop[face.faceID] = data; } else { facesWithoutCrops[face.faceID] = face.detection.box; @@ -102,7 +159,11 @@ Future?> getCachedFaceCrops( if (computedCrop != null) { faceIdToCrop[entry.key] = computedCrop; if (useFullFile) { - _faceCropCache.put(entry.key, computedCrop); + await _putCachedCropForFaceID( + entry.key, + computedCrop, + personOrClusterID, + ); final faceCropCacheFile = cachedFaceCropPath(entry.key); faceCropCacheFile.writeAsBytes(computedCrop).ignore(); } else { @@ -191,11 +252,10 @@ Future precomputeClusterFaceCrop( } void checkStopTryingToGenerateFaceThumbnails( - EnteFile file, { + int fileID, { bool useFullFile = true, }) { - final taskId = - [file.uploadedFileID!, useFullFile ? "-full" : "-thumbnail"].join(); + final taskId = [fileID, useFullFile ? "-full" : "-thumbnail"].join(); if (useFullFile) { _queueFullFileFaceGenerations.removeTask(taskId); } else { diff --git a/mobile/lib/utils/file_download_util.dart b/mobile/lib/utils/file_download_util.dart index 420ad34713..628c08e750 100644 --- a/mobile/lib/utils/file_download_util.dart +++ b/mobile/lib/utils/file_download_util.dart @@ -1,5 +1,4 @@ import "dart:async"; -import "dart:collection"; import 'dart:io'; import 'package:dio/dio.dart'; @@ -16,6 +15,9 @@ import "package:photos/events/local_photos_updated_event.dart"; import 'package:photos/models/file/file.dart'; import "package:photos/models/file/file_type.dart"; import "package:photos/models/ignored_file.dart"; +import "package:photos/module/download/file_url.dart"; +import "package:photos/module/download/task.dart"; +import "package:photos/service_locator.dart"; import "package:photos/services/collections_service.dart"; import "package:photos/services/ignored_files_service.dart"; import "package:photos/services/sync/local_sync_service.dart"; @@ -48,7 +50,7 @@ Future downloadAndDecryptPublicFile( if (authJWTToken != null) "X-Auth-Access-Token-JWT": authJWTToken, }; final response = (await NetworkClient.instance.getDio().download( - file.publicDownloadUrl, + FileUrl.getUrl(file.uploadedFileID!, FileUrlType.publicDownload), encryptedFilePath, options: Options( headers: headers, @@ -114,30 +116,48 @@ Future downloadAndDecrypt( _logger .info('$logPrefix starting download ${formatBytes(file.fileSize ?? 0)}'); final String tempDir = Configuration.instance.getTempDirectory(); - final String encryptedFilePath = "$tempDir${file.generatedID}.encrypted"; - final encryptedFile = File(encryptedFilePath); + String encryptedFilePath = "$tempDir${file.generatedID}.encrypted"; + File encryptedFile = File(encryptedFilePath); final startTime = DateTime.now().millisecondsSinceEpoch; try { - final response = await NetworkClient.instance.getDio().download( - file.downloadUrl, - encryptedFilePath, - options: Options( - headers: {"X-Auth-Token": Configuration.instance.getToken()}, - ), - onReceiveProgress: (a, b) { - if (kDebugMode && a >= 0 && b >= 0) { - // _logger.fine( - // "$logPrefix download progress: ${formatBytes(a)} / ${formatBytes(b)}", - // ); - } - progressCallback?.call(a, b); - }, - ); - if (response.statusCode != 200 || !encryptedFile.existsSync()) { - _logger.warning('$logPrefix download failed ${response.toString()}'); - return null; + if (downloadManager.enableResumableDownload(file.fileSize)) { + final DownloadResult result = await downloadManager.download( + file.uploadedFileID!, + file.displayName, + file.fileSize!, + ); + if (result.success) { + encryptedFilePath = result.task.filePath!; + encryptedFile = File(encryptedFilePath); + } else { + _logger.warning( + '$logPrefix download failed ${result.task.error} ${result.task.status}', + ); + return null; + } + } else { + // If the file is small, download it directly to the final location + final response = await NetworkClient.instance.getDio().download( + file.downloadUrl, + encryptedFilePath, + options: Options( + headers: {"X-Auth-Token": Configuration.instance.getToken()}, + ), + onReceiveProgress: (a, b) { + if (kDebugMode && a >= 0 && b >= 0) { + // _logger.fine( + // "$logPrefix download progress: ${formatBytes(a)} / ${formatBytes(b)}", + // ); + } + progressCallback?.call(a, b); + }, + ); + if (response.statusCode != 200 || !encryptedFile.existsSync()) { + _logger.warning('$logPrefix download failed ${response.toString()}'); + return null; + } } final int sizeInBytes = file.fileSize ?? await encryptedFile.length(); @@ -277,37 +297,3 @@ Future _saveLivePhotoOnDroid( ); await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]); } - -class DownloadQueue { - final int maxConcurrent; - final Queue Function()> _queue = Queue(); - int _runningTasks = 0; - - DownloadQueue({this.maxConcurrent = 5}); - - Future add(Future Function() task) async { - final completer = Completer(); - _queue.add(() async { - try { - await task(); - completer.complete(); - } catch (e) { - completer.completeError(e); - } finally { - _runningTasks--; - _processQueue(); - } - return completer.future; - }); - _processQueue(); - return completer.future; - } - - void _processQueue() { - while (_runningTasks < maxConcurrent && _queue.isNotEmpty) { - final task = _queue.removeFirst(); - _runningTasks++; - task(); - } - } -} diff --git a/mobile/lib/utils/file_uploader.dart b/mobile/lib/utils/file_uploader.dart index e7f43e370e..6a538a0fa9 100644 --- a/mobile/lib/utils/file_uploader.dart +++ b/mobile/lib/utils/file_uploader.dart @@ -36,7 +36,6 @@ import "package:photos/module/upload/service/multipart.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/account/user_service.dart"; import 'package:photos/services/collections_service.dart'; -import "package:photos/services/preview_video_store.dart"; import 'package:photos/services/sync/local_sync_service.dart'; import 'package:photos/services/sync/sync_service.dart'; import "package:photos/utils/exif_util.dart"; @@ -99,8 +98,6 @@ class FileUploader { static FileUploader instance = FileUploader._privateConstructor(); - static final _previewVideoStore = PreviewVideoStore.instance; - Future init(SharedPreferences preferences, bool isBackground) async { _prefs = preferences; _processType = @@ -472,14 +469,6 @@ class FileUploader { } } - void _uploadPreview(EnteFile file) { - if (file.fileType == FileType.video) { - unawaited( - _previewVideoStore.chunkAndUploadVideo(null, file), - ); - } - } - Future _tryToUpload( EnteFile file, int collectionID, @@ -814,9 +803,6 @@ class FileUploader { } await FilesDB.instance.update(remoteFile); } - if (PreviewVideoStore.instance.isVideoStreamingEnabled) { - _uploadPreview(file); - } await UploadLocksDB.instance.deleteMultipartTrack(lockKey); Bus.instance.fire( @@ -845,8 +831,8 @@ class FileUploader { if ((e is StorageLimitExceededError || e is FileTooLargeForPlanError || e is NoActiveSubscriptionError)) { - // file upload can be be retried in such cases without user intervention - uploadHardFailure = false; + // file upload can not be retried in such cases without user intervention + uploadHardFailure = true; } if (isMultipartUpload && isPutOrUpdateFileError(e)) { await UploadLocksDB.instance.deleteMultipartTrack(lockKey); @@ -907,12 +893,15 @@ class FileUploader { files. if the link is successful, it returns true otherwise false. When false, we should go ahead and re-upload or update the file. It performs following checks: - a) Uploaded file with same localID and destination collection. Delete the - fileToUpload entry + a) Target file with same localID and destination collection exists. Delete the + fileToUpload entry. If target file is sandbox file, then we skip localID match + check. b) Uploaded file in any collection but with missing localID. Update the localID for uploadedFile and delete the fileToUpload entry c) A uploaded file exist with same localID but in a different collection. - Add a symlink in the destination collection and update the fileToUpload + Add a symlink in the destination collection and update the fileToUpload. + If target file is sandbox file, then we skip localID match + check. d) File already exists but different localID. Re-upload In case the existing files already have local identifier, which is different from the {fileToUpload}, then most probably device has @@ -931,6 +920,7 @@ class FileUploader { ); return Tuple2(false, fileToUpload); } + final bool isSandBoxFile = fileToUpload.isSharedMediaToAppSandbox; final List existingUploadedFiles = await FilesDB.instance.getUploadedFilesWithHashes( @@ -947,12 +937,13 @@ class FileUploader { final EnteFile? sameLocalSameCollection = existingUploadedFiles.firstWhereOrNull( (e) => - e.collectionID == toCollectionID && e.localID == fileToUpload.localID, + e.collectionID == toCollectionID && + (e.localID == fileToUpload.localID || isSandBoxFile), ); if (sameLocalSameCollection != null) { _logger.fine( - "sameLocalSameCollection: \n toUpload ${fileToUpload.tag} " - "\n existing: ${sameLocalSameCollection.tag}", + "sameLocalSameCollection: toUpload ${fileToUpload.tag} " + "existing: ${sameLocalSameCollection.tag} $isSandBoxFile", ); // should delete the fileToUploadEntry if (fileToUpload.generatedID != null) { @@ -1005,12 +996,13 @@ class FileUploader { final EnteFile? fileExistsButDifferentCollection = existingUploadedFiles.firstWhereOrNull( (e) => - e.collectionID != toCollectionID && e.localID == fileToUpload.localID, + e.collectionID != toCollectionID && + (e.localID == fileToUpload.localID || isSandBoxFile), ); if (fileExistsButDifferentCollection != null) { _logger.fine( - "fileExistsButDifferentCollection: \n toUpload ${fileToUpload.tag} " - "\n existing: ${fileExistsButDifferentCollection.tag}", + "fileExistsButDifferentCollection: toUpload ${fileToUpload.tag} " + "existing: ${fileExistsButDifferentCollection.tag} $isSandBoxFile", ); final linkedFile = await CollectionsService.instance .linkLocalFileToExistingUploadedFileInAnotherCollection( diff --git a/mobile/lib/utils/image_ml_util.dart b/mobile/lib/utils/image_ml_util.dart index bdd84d96f1..ba922fc3fb 100644 --- a/mobile/lib/utils/image_ml_util.dart +++ b/mobile/lib/utils/image_ml_util.dart @@ -32,12 +32,25 @@ final List> gaussianKernel = const maxKernelSize = gaussianKernelSize; const maxKernelRadius = maxKernelSize ~/ 2; -Future<(Image, Uint8List)> decodeImageFromPath(String imagePath) async { +class DecodedImage { + final Image image; + final Uint8List? rawRgbaBytes; + + const DecodedImage(this.image, [this.rawRgbaBytes]); +} + +Future decodeImageFromPath( + String imagePath, { + required bool includeRgbaBytes, +}) async { try { final imageData = await File(imagePath).readAsBytes(); final image = await decodeImageFromData(imageData); + if (!includeRgbaBytes) { + return DecodedImage(image); + } final rawRgbaBytes = await _getRawRgbaBytes(image); - return (image, rawRgbaBytes); + return DecodedImage(image, rawRgbaBytes); } catch (e, s) { final format = imagePath.split('.').last; _logger.info( @@ -50,9 +63,12 @@ Future<(Image, Uint8List)> decodeImageFromPath(String imagePath) async { format: CompressFormat.jpeg, ); final image = await decodeImageFromData(convertedData!); - final rawRgbaBytes = await _getRawRgbaBytes(image); _logger.info('Conversion successful, jpeg decoded'); - return (image, rawRgbaBytes); + if (!includeRgbaBytes) { + return DecodedImage(image); + } + final rawRgbaBytes = await _getRawRgbaBytes(image); + return DecodedImage(image, rawRgbaBytes); } catch (e) { _logger.severe( 'Error decoding image of format $format on ${Platform.isAndroid ? "Android" : "iOS"}', @@ -122,12 +138,16 @@ Future _getByteDataFromImage( /// /// Returns a [Uint8List] image, in png format. Future> generateFaceThumbnailsUsingCanvas( - Uint8List imageData, + String imagePath, List faceBoxes, ) async { int i = 0; // Index of the faceBox, initialized here for logging purposes try { - final Image img = await decodeImageFromData(imageData); + final decodedImage = await decodeImageFromPath( + imagePath, + includeRgbaBytes: false, + ); + final Image img = decodedImage.image; final futureFaceThumbnails = >[]; for (final faceBox in faceBoxes) { // Note that the faceBox values are relative to the image size, so we need to convert them to absolute values first diff --git a/mobile/lib/utils/local_settings.dart b/mobile/lib/utils/local_settings.dart index 72bb8a2f1b..ab63e2ab81 100644 --- a/mobile/lib/utils/local_settings.dart +++ b/mobile/lib/utils/local_settings.dart @@ -8,6 +8,16 @@ enum AlbumSortKey { lastUpdated, } +enum AlbumSortDirection { + ascending, + descending, +} + +enum AlbumViewType { + grid, + list, +} + class LocalSettings { static const kCollectionSortPref = "collection_sort_pref"; static const kPhotoGridSize = "photo_grid_size"; @@ -16,6 +26,8 @@ class LocalSettings { static const kRateUsShownCount = "rate_us_shown_count"; static const kEnableMultiplePart = "ls.enable_multiple_part"; static const kCuratedMemoriesEnabled = "ls.curated_memories_enabled"; + static const kOnThisDayNotificationsEnabled = + "ls.on_this_day_notifications_enabled"; static const kRateUsPromptThreshold = 2; static const shouldLoopVideoKey = "video.should_loop"; static const onGuestViewKey = "on_guest_view"; @@ -23,6 +35,8 @@ class LocalSettings { "has_configured_links_in_app_permission"; static const _hideSharedItemsFromHomeGalleryTag = "hide_shared_items_from_home_gallery"; + static const kCollectionViewType = "collection_view_type"; + static const kCollectionSortDirection = "collection_sort_direction"; final SharedPreferences _prefs; @@ -36,6 +50,24 @@ class LocalSettings { return _prefs.setInt(kCollectionSortPref, key.index); } + Future setAlbumViewType(AlbumViewType viewType) async { + await _prefs.setInt(kCollectionViewType, viewType.index); + } + + AlbumViewType albumViewType() { + final index = _prefs.getInt(kCollectionViewType) ?? 0; + return AlbumViewType.values[index]; + } + + AlbumSortDirection albumSortDirection() { + return AlbumSortDirection + .values[_prefs.getInt(kCollectionSortDirection) ?? 1]; + } + + Future setAlbumSortDirection(AlbumSortDirection direction) { + return _prefs.setInt(kCollectionSortDirection, direction.index); + } + int getPhotoGridSize() { if (_prefs.containsKey(kPhotoGridSize)) { return _prefs.getInt(kPhotoGridSize)!; @@ -89,21 +121,17 @@ class LocalSettings { return value; } - bool get userEnabledMultiplePart => - _prefs.getBool(kEnableMultiplePart) ?? false; + bool get isOnThisDayNotificationsEnabled => + _prefs.getBool(kOnThisDayNotificationsEnabled) ?? true; - Future autoEnableMultiplePart(int rolloutPercentage) async { - if (_prefs.containsKey(kEnableMultiplePart)) { - return; - } - rolloutPercentage = rolloutPercentage.clamp(0, 100); - final randomValue = DateTime.now().millisecondsSinceEpoch % 100; - await _prefs.setBool( - kEnableMultiplePart, - randomValue < rolloutPercentage, - ); + Future setOnThisDayNotificationsEnabled(bool value) async { + await _prefs.setBool(kOnThisDayNotificationsEnabled, value); + return value; } + bool get userEnabledMultiplePart => + _prefs.getBool(kEnableMultiplePart) ?? true; + Future setUserEnabledMultiplePart(bool value) async { await _prefs.setBool(kEnableMultiplePart, value); return value; diff --git a/mobile/lib/utils/ml_util.dart b/mobile/lib/utils/ml_util.dart index b1d18307da..f8e704e171 100644 --- a/mobile/lib/utils/ml_util.dart +++ b/mobile/lib/utils/ml_util.dart @@ -15,7 +15,6 @@ import "package:photos/models/ml/face/dimension.dart"; import "package:photos/models/ml/face/face.dart"; import "package:photos/models/ml/ml_versions.dart"; import "package:photos/service_locator.dart"; -import "package:photos/services/filedata/filedata_service.dart"; import "package:photos/services/filedata/model/file_data.dart"; import "package:photos/services/machine_learning/face_ml/face_recognition_service.dart"; import "package:photos/services/machine_learning/ml_exceptions.dart"; @@ -205,7 +204,7 @@ Stream> fetchEmbeddingsAndInstructions( pendingIndex[instruction.file.uploadedFileID!] = instruction; } _logger.info("fetching embeddings for ${ids.length} files"); - final res = await FileDataService.instance.getFilesData(ids); + final res = await fileDataService.getFilesData(ids); _logger.info("embeddingResponse ${res.debugLog()}"); final List faces = []; final List clipEmbeddings = []; @@ -410,7 +409,10 @@ Future analyzeImageStatic(Map args) async { final startTime = DateTime.now(); // Decode the image once to use for both face detection and alignment - final (image, rawRgbaBytes) = await decodeImageFromPath(imagePath); + final decodedImage = + await decodeImageFromPath(imagePath, includeRgbaBytes: true); + final image = decodedImage.image; + final rawRgbaBytes = decodedImage.rawRgbaBytes!; final decodedImageSize = Dimensions(height: image.height, width: image.width); final result = MLResult.fromEnteFileID(enteFileID); diff --git a/mobile/lib/utils/person_contact_linking_util.dart b/mobile/lib/utils/person_contact_linking_util.dart index cf8877269e..aee0349f6f 100644 --- a/mobile/lib/utils/person_contact_linking_util.dart +++ b/mobile/lib/utils/person_contact_linking_util.dart @@ -1,4 +1,11 @@ +import "package:collection/collection.dart"; +import "package:flutter/widgets.dart"; +import "package:photos/l10n/l10n.dart"; +import "package:photos/models/ml/face/person.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; +import "package:photos/ui/viewer/people/people_page.dart"; +import "package:photos/utils/dialog_util.dart"; +import "package:photos/utils/navigation_util.dart"; Future checkIfEmailAlreadyAssignedToAPerson( String email, @@ -11,3 +18,33 @@ Future checkIfEmailAlreadyAssignedToAPerson( } return false; } + +Future showAlreadyLinkedEmailDialog( + BuildContext context, + String email, +) async { + final persons = await PersonService.instance.getPersons(); + final PersonEntity? person = persons.firstWhereOrNull( + (person) => person.data.email == email, + ); + if (person == null) { + return; + } + + await showChoiceActionSheet( + context, + title: context.l10n.error, + body: context.l10n.editEmailAlreadyLinked(person.data.name), + firstButtonLabel: context.l10n.viewPersonToUnlink(person.data.name), + firstButtonOnTap: () async { + await routeToPage( + context, + PeoplePage( + person: person, + searchResult: null, + ), + ); + }, + isCritical: false, + ); +} diff --git a/mobile/lib/utils/standalone/simple_task_queue.dart b/mobile/lib/utils/standalone/simple_task_queue.dart new file mode 100644 index 0000000000..1f11c2da2a --- /dev/null +++ b/mobile/lib/utils/standalone/simple_task_queue.dart @@ -0,0 +1,41 @@ +import "dart:async"; +import "dart:collection"; + +// SimpleTaskQueue is a simple task queue that allows you to add tasks +// and run them concurrently up to a specified limit. It doesn't support +// task cancellation or timeout, but it can be used for simple +// asynchronous task management. +// See [TaskQueue] for a more advanced implementation with +class SimpleTaskQueue { + final int maxConcurrent; + final Queue Function()> _queue = Queue(); + int _runningTasks = 0; + + SimpleTaskQueue({this.maxConcurrent = 5}); + + Future add(Future Function() task) async { + final completer = Completer(); + _queue.add(() async { + try { + await task(); + completer.complete(); + } catch (e) { + completer.completeError(e); + } finally { + _runningTasks--; + _processQueue(); + } + return completer.future; + }); + _processQueue(); + return completer.future; + } + + void _processQueue() { + while (_runningTasks < maxConcurrent && _queue.isNotEmpty) { + final task = _queue.removeFirst(); + _runningTasks++; + task(); + } + } +} diff --git a/mobile/lib/utils/standalone/task_queue.dart b/mobile/lib/utils/standalone/task_queue.dart index a5a5eb0dfb..d82f58a9eb 100644 --- a/mobile/lib/utils/standalone/task_queue.dart +++ b/mobile/lib/utils/standalone/task_queue.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import "dart:developer"; import 'package:collection/collection.dart'; @@ -216,7 +217,7 @@ class TaskQueue { if (!queueItem.completer.isCompleted) { queueItem.completer.completeError(e); } - print('Task error: $e'); + log('Task error: $e'); } finally { // Mark the task as completed _runningTasks.remove(taskId); diff --git a/mobile/lib/utils/thumbnail_util.dart b/mobile/lib/utils/thumbnail_util.dart index 17d52c6432..169137e880 100644 --- a/mobile/lib/utils/thumbnail_util.dart +++ b/mobile/lib/utils/thumbnail_util.dart @@ -13,6 +13,7 @@ import 'package:photos/core/constants.dart'; import 'package:photos/core/errors.dart'; import 'package:photos/core/network/network.dart'; import 'package:photos/models/file/file.dart'; +import "package:photos/module/download/file_url.dart"; import "package:photos/services/collections_service.dart"; import "package:photos/utils/file_key.dart"; import 'package:photos/utils/file_uploader_util.dart'; @@ -173,7 +174,10 @@ Future _downloadAndDecryptThumbnail(FileDownloadItem item) async { }; encryptedThumbnail = (await NetworkClient.instance.getDio().get( - file.pubPreviewUrl, + FileUrl.getUrl( + file.uploadedFileID!, + FileUrlType.publicThumbnail, + ), options: Options( headers: headers, responseType: ResponseType.bytes, @@ -182,7 +186,10 @@ Future _downloadAndDecryptThumbnail(FileDownloadItem item) async { .data; } else { encryptedThumbnail = (await NetworkClient.instance.getDio().get( - file.thumbnailUrl, + FileUrl.getUrl( + file.uploadedFileID!, + FileUrlType.thumbnail, + ), options: Options( headers: {"X-Auth-Token": Configuration.instance.getToken()}, responseType: ResponseType.bytes, diff --git a/mobile/plugins/ente_cast_normal/pubspec.yaml b/mobile/plugins/ente_cast_normal/pubspec.yaml index c97d70a84b..141c2e9e7b 100644 --- a/mobile/plugins/ente_cast_normal/pubspec.yaml +++ b/mobile/plugins/ente_cast_normal/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: git: url: https://github.com/guyluz11/flutter_cast.git ref: multicast_version + commit: 1f39cd4 ente_cast: path: ../ente_cast flutter: diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 36d05a086a..c018fec35f 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -933,26 +933,34 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35" + sha256: b94a50aabbe56ef254f95f3be75640f99120429f0a153b2dc30143cffc9bfdf3 url: "https://pub.dev" source: hosted - version: "17.2.4" + version: "19.2.1" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af + sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5 url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "6.0.0" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" + sha256: "2569b973fc9d1f63a37410a9f7c1c552081226c597190cb359ef5d5762d1631c" url: "https://pub.dev" source: hosted - version: "7.2.0" + version: "9.0.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: f8fc0652a601f83419d623c85723a3e82ad81f92b33eaa9bcc21ea1b94773e6e + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -1108,6 +1116,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_timezone: + dependency: "direct main" + description: + name: flutter_timezone + sha256: bc286cecb0366d88e6c4644e3962ebd1ce1d233abc658eb1e0cd803389f84b64 + url: "https://pub.dev" + source: hosted + version: "4.1.0" flutter_web_plugins: dependency: transitive description: flutter @@ -2072,7 +2088,7 @@ packages: source: hosted version: "2.1.0" protobuf: - dependency: "direct main" + dependency: "direct overridden" description: name: protobuf sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" @@ -2541,13 +2557,13 @@ packages: source: hosted version: "0.6.4" timezone: - dependency: transitive + dependency: "direct main" description: name: timezone - sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 url: "https://pub.dev" source: hosted - version: "0.9.4" + version: "0.10.1" timing: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 5cb34c06dc..817d29b801 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -12,7 +12,7 @@ description: ente photos application # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.0.11+1042 +version: 1.1.2+1056 publish_to: none environment: @@ -86,7 +86,7 @@ dependencies: flutter_email_sender: ^7.0.0 flutter_image_compress: ^2.4.0 flutter_inappwebview: ^6.1.4 - flutter_local_notifications: ^17.2.2 + flutter_local_notifications: ^19.2.1 flutter_localizations: sdk: flutter flutter_map: ^6.2.0 @@ -103,6 +103,7 @@ dependencies: flutter_sodium: flutter_staggered_grid_view: ^0.6.2 flutter_svg: ^2.0.10+1 + flutter_timezone: ^4.1.0 fluttertoast: ^8.0.6 fraction: ^5.0.2 freezed_annotation: ^2.4.1 @@ -170,7 +171,6 @@ dependencies: git: url: https://github.com/eddyuan/privacy_screen.git ref: 855418e - protobuf: ^3.1.0 receive_sharing_intent: # pub.dev is behind git: url: https://github.com/KasemJaffer/receive_sharing_intent.git @@ -193,6 +193,7 @@ dependencies: syncfusion_flutter_sliders: ^25.2.5 synchronized: ^3.3.0+3 system_info_plus: ^0.0.6 + timezone: ^0.10.0 tuple: ^2.0.0 ua_client_hints: ^1.4.0 url_launcher: ^6.3.0 @@ -237,6 +238,7 @@ dependency_overrides: git: url: https://github.com/media-kit/media-kit path: media_kit_video + protobuf: ^3.1.0 video_player: # remove this dep as soon as we move to one player for all git: url: https://github.com/ente-io/packages.git diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index 7070b8af61..742d098397 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -5,7 +5,6 @@ import ( "database/sql" b64 "encoding/base64" "fmt" - "github.com/ente-io/museum/pkg/controller/collections" "net/http" "os" "os/signal" @@ -15,6 +14,8 @@ import ( "syscall" "time" + "github.com/ente-io/museum/pkg/controller/collections" + "github.com/ente-io/museum/ente/base" "github.com/ente-io/museum/pkg/controller/emergency" "github.com/ente-io/museum/pkg/controller/file_copy" @@ -922,8 +923,8 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionR }) schedule(c, "@every 24h", func() { - _ = userAuthRepo.RemoveDeletedTokens(timeUtil.MicrosecondBeforeDays(30)) - _ = castDb.DeleteOldSessions(context.Background(), timeUtil.MicrosecondBeforeDays(7)) + _ = userAuthRepo.RemoveDeletedTokens(timeUtil.MicrosecondsBeforeDays(30)) + _ = castDb.DeleteOldSessions(context.Background(), timeUtil.MicrosecondsBeforeDays(7)) _ = publicCollectionRepo.CleanupAccessHistory(context.Background()) }) @@ -990,6 +991,10 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionR emailNotificationCtrl.SendStorageLimitExceededMails() }) + scheduleAndRun(c, "@every 24h", func() { + emailNotificationCtrl.SayHelloToCustomers() + }) + schedule(c, "@every 1m", func() { pushController.SendPushes() }) @@ -1010,7 +1015,7 @@ func cors() gin.HandlerFunc { return func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", c.GetHeader("Origin")) c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") - c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, X-Auth-Token, X-Auth-Access-Token, X-Cast-Access-Token, X-Auth-Access-Token-JWT, X-Client-Package, X-Client-Version, Authorization, accept, origin, Cache-Control, X-Requested-With, upgrade-insecure-requests") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, X-Auth-Token, X-Auth-Access-Token, X-Cast-Access-Token, X-Auth-Access-Token-JWT, X-Client-Package, X-Client-Version, Authorization, accept, origin, Cache-Control, X-Requested-With, upgrade-insecure-requests, Range") c.Writer.Header().Set("Access-Control-Expose-Headers", "X-Request-Id") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH, DELETE") c.Writer.Header().Set("Access-Control-Max-Age", "1728000") diff --git a/server/ente/billing.go b/server/ente/billing.go index 9909b9f052..5e212c5cb8 100644 --- a/server/ente/billing.go +++ b/server/ente/billing.go @@ -30,7 +30,7 @@ const ( Period3Years = "3" Period5Years = "5" - + Period10Years = "10" // FamilyPlanProductID is the product ID of family (internal employees & their friends & family) plan @@ -127,6 +127,7 @@ type Subscription struct { // LinkedPurchaseToken on PlayStore , OriginalTransactionID on AppStore and SubscriptionID on Stripe OriginalTransactionID string `json:"originalTransactionID"` ExpiryTime int64 `json:"expiryTime"` + UpgradedAt int64 `json:"upgradedAt,omitempty"` PaymentProvider PaymentProvider `json:"paymentProvider"` Attributes SubscriptionAttributes `json:"attributes"` Price string `json:"price"` diff --git a/server/ente/filedata/filedata.go b/server/ente/filedata/filedata.go index 5b6ec70edd..268d60073a 100644 --- a/server/ente/filedata/filedata.go +++ b/server/ente/filedata/filedata.go @@ -26,6 +26,7 @@ type Entity struct { Type ente.ObjectType `json:"type"` EncryptedData string `json:"encryptedData"` DecryptionHeader string `json:"decryptionHeader"` + UpdatedAt int64 `json:"updatedAt"` } type FDDiffRequest struct { @@ -104,19 +105,32 @@ func (g *GetPreviewURLRequest) Validate() error { } type PreviewUploadUrlRequest struct { - FileID int64 `form:"fileID" binding:"required"` - Type ente.ObjectType `form:"type" binding:"required"` + FileID int64 `form:"fileID" binding:"required"` + Type ente.ObjectType `form:"type" binding:"required"` + IsMultiPart bool `form:"isMultiPart"` + Count *int64 `form:"count"` } type PreviewUploadUrl struct { - ObjectID string `json:"objectID" binding:"required"` - Url string `json:"url" binding:"required"` + ObjectID string `json:"objectID" binding:"required"` + Url *string `json:"url,omitempty"` + PartURLs *[]string `json:"partURLs,omitempty"` + CompleteURL *string `json:"completeURL,omitempty"` } func (g *PreviewUploadUrlRequest) Validate() error { if g.Type != ente.PreviewVideo && g.Type != ente.PreviewImage { return ente.NewBadRequestWithMessage(fmt.Sprintf("unsupported object type %s", g.Type)) } + if !g.IsMultiPart { + return nil + } + if g.Count == nil { + return ente.NewBadRequestWithMessage("count is required for multipart upload") + } else if *g.Count <= 0 || *g.Count > 10000 { + return ente.NewBadRequestWithMessage("invalid count, should be between 1 and 10000") + } + return nil } diff --git a/server/ente/filedata/putfiledata.go b/server/ente/filedata/putfiledata.go index a4b4f7bf5b..43df9c68d1 100644 --- a/server/ente/filedata/putfiledata.go +++ b/server/ente/filedata/putfiledata.go @@ -11,6 +11,8 @@ type PutFileDataRequest struct { EncryptedData *string `json:"encryptedData,omitempty"` DecryptionHeader *string `json:"decryptionHeader,omitempty"` Version *int `json:"version,omitempty"` + // Used to ensure that the client has correct state before it tries to update the metadata + LastUpdatedAt *int64 `json:"lastUpdatedAt,omitempty"` } func (r PutFileDataRequest) isEncDataPresent() bool { diff --git a/server/mail-templates/customer_hello.html b/server/mail-templates/customer_hello.html new file mode 100644 index 0000000000..cd09a80df4 --- /dev/null +++ b/server/mail-templates/customer_hello.html @@ -0,0 +1,149 @@ + + + + + + + +
 
+
+

Hi there,

+ +

Vishnu here, CEO of Ente.

+ +

Thank you for checking out Ente Photos, and upgrading to a paid plan.

+ +

This is my personal email address. If you need help with anything or + have feedback to share, please write back - we want this to be a great + experience for you!

+ +

Best, +
+ Vishnu +
+ Founder & CEO at Ente +

+
+
+ + + diff --git a/server/mail-templates/mobile_app_first_upload.html b/server/mail-templates/mobile_app_first_upload.html index 5d05827495..6947b5225b 100644 --- a/server/mail-templates/mobile_app_first_upload.html +++ b/server/mail-templates/mobile_app_first_upload.html @@ -113,15 +113,15 @@

Congratulations on preserving your first memory!

- Did you know that we will be keeping 3 copies of your data, at 3 - different locations so they are as safe as they can be? One of these - copies will in fact be preserved in an underground fallout shelter! + Did you know that if you upgrade to a paid plan, we will keep 3 copies + of your data, at 3 locations? One of these is in an underground fallout + shelter!

- While we work our magic, you could turn on - Machine Learning to - see the magic you can do with your photos. + Even on the free plan, you can turn on + machine learning for + a private magic show.

diff --git a/server/mail-templates/web_app_first_upload.html b/server/mail-templates/web_app_first_upload.html index aa8684d6ba..52a4e9b5ca 100644 --- a/server/mail-templates/web_app_first_upload.html +++ b/server/mail-templates/web_app_first_upload.html @@ -113,15 +113,15 @@

Congratulations on preserving your first memory!

- Did you know that we will be keeping 3 copies of your data, at 3 - different locations so they are as safe as they can be? One of these - copies will in fact be preserved in an underground fallout shelter! + Did you know that if you upgrade to a paid plan, we will keep 3 copies + of your data, at 3 locations? One of these is in an underground fallout + shelter!

- While we work our magic, you could turn on - Machine Learning to - see the magic you can do with your photos. + Even on the free plan, you can turn on + machine learning for + a private magic show.

diff --git a/server/migrations/99_add_upgraded_at.down.sql b/server/migrations/99_add_upgraded_at.down.sql new file mode 100644 index 0000000000..6c5789be42 --- /dev/null +++ b/server/migrations/99_add_upgraded_at.down.sql @@ -0,0 +1 @@ +ALTER TABLE subscriptions DROP COLUMN upgraded_at; diff --git a/server/migrations/99_add_upgraded_at.up.sql b/server/migrations/99_add_upgraded_at.up.sql new file mode 100644 index 0000000000..1c8dc3d10f --- /dev/null +++ b/server/migrations/99_add_upgraded_at.up.sql @@ -0,0 +1 @@ +ALTER TABLE subscriptions ADD COLUMN upgraded_at BIGINT DEFAULT 0; diff --git a/server/migrations/99_alter_file_data_column_type.down.sql b/server/migrations/99_alter_file_data_column_type.down.sql new file mode 100644 index 0000000000..502d906f19 --- /dev/null +++ b/server/migrations/99_alter_file_data_column_type.down.sql @@ -0,0 +1,13 @@ +DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM file_data + WHERE obj_size IS NOT NULL + AND (obj_size > 2147483647 OR obj_size < -2147483648) + ) THEN + RAISE EXCEPTION 'Cannot downgrade - some values exceed integer limits'; + END IF; + END $$; + +ALTER TABLE file_data + ALTER COLUMN obj_size TYPE integer USING obj_size::integer; \ No newline at end of file diff --git a/server/migrations/99_alter_file_data_column_type.up.sql b/server/migrations/99_alter_file_data_column_type.up.sql new file mode 100644 index 0000000000..efe82821b0 --- /dev/null +++ b/server/migrations/99_alter_file_data_column_type.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE file_data + ALTER COLUMN obj_size TYPE bigint USING obj_size::bigint; \ No newline at end of file diff --git a/server/pkg/api/authenticator.go b/server/pkg/api/authenticator.go index 6b071cf81d..645912cbab 100644 --- a/server/pkg/api/authenticator.go +++ b/server/pkg/api/authenticator.go @@ -3,6 +3,7 @@ package api import ( "fmt" "net/http" + "time" "github.com/ente-io/museum/ente" model "github.com/ente-io/museum/ente/authenticator" @@ -105,7 +106,9 @@ func (h *AuthenticatorHandler) GetDiff(c *gin.Context) { handler.Error(c, stacktrace.Propagate(err, "Failed to fetch authenticator entity diff")) return } + c.JSON(http.StatusOK, gin.H{ - "diff": entities, + "diff": entities, + "timestamp": time.Now().UnixMicro(), }) } diff --git a/server/pkg/controller/billing.go b/server/pkg/controller/billing.go index 8b9b3f75f5..ff57c94775 100644 --- a/server/pkg/controller/billing.go +++ b/server/pkg/controller/billing.go @@ -285,6 +285,9 @@ func (c *BillingController) VerifySubscription( } } } + if isUpgradingFromFreePlan { + newSubscription.UpgradedAt = time.Microseconds() + } err = c.BillingRepo.ReplaceSubscription( currentSubscription.ID, newSubscription, diff --git a/server/pkg/controller/email/email_notification.go b/server/pkg/controller/email/email_notification.go index 328a094ce4..a825f61b12 100644 --- a/server/pkg/controller/email/email_notification.go +++ b/server/pkg/controller/email/email_notification.go @@ -18,6 +18,11 @@ const ( MobileAppFirstUploadTemplate = "mobile_app_first_upload.html" FirstUploadEmailSubject = "Congratulations! 🎉" + CustomerHelloMailLock = "customer_hello_mail_lock" + CustomerHelloSubject = "Hello from Ente" + CustomerHelloTemplate = "customer_hello.html" + CustomerHelloTemplateID = "customer_hello" + StorageLimitExceededMailLock = "storage_limit_exceeded_mail_lock" StorageLimitExceededTemplateID = "storage_limit_exceeded" StorageLimitExceededTemplate = "storage_limit_exceeded.html" @@ -158,6 +163,45 @@ func (c *EmailNotificationController) OnSubscriptionCancelled(userID int64) { } } +// SayHelloToCustomers sends an email to check in with customers who upgraded 7 +// days ago. +func (c *EmailNotificationController) SayHelloToCustomers() { + log.Info("Running SayHelloToCustomers") + lockStatus := c.LockController.TryLock(CustomerHelloMailLock, time.MicrosecondsAfterHours(24)) + if !lockStatus { + log.Error("Could not acquire lock to send customer hellos") + return + } + defer c.LockController.ReleaseLock(CustomerHelloMailLock) + + users, err := c.UserRepo.GetUsersWhoUpgradedNDaysAgo(7) + if err != nil { + log.Error("Error while fetching users", err) + return + } + for _, u := range users { + lastNotificationTime, err := c.NotificationHistoryRepo.GetLastNotificationTime(u.ID, CustomerHelloTemplateID) + logger := log.WithFields(log.Fields{ + "user_id": u.ID, + "email": u.Email, + }) + if err != nil { + logger.Error("Could not fetch last notification time", err) + continue + } + if lastNotificationTime > 0 { + continue + } + logger.Info("Sending hello email to customer") + err = email.SendTemplatedEmail([]string{u.Email}, "vishnu@ente.io", "vishnu@ente.io", CustomerHelloSubject, CustomerHelloTemplate, nil, nil) + if err != nil { + logger.Info("Error notifying", err) + continue + } + c.NotificationHistoryRepo.SetLastNotificationTimeToNow(u.ID, CustomerHelloTemplateID) + } +} + func (c *EmailNotificationController) SendStorageLimitExceededMails() { if c.isSendingStorageLimitExceededMails { log.Info("Skipping sending storage limit exceeded mails as another instance is still running") diff --git a/server/pkg/controller/file.go b/server/pkg/controller/file.go index 98e58dd589..1937afa0ea 100644 --- a/server/pkg/controller/file.go +++ b/server/pkg/controller/file.go @@ -59,7 +59,7 @@ type FileController struct { const StorageOverflowAboveSubscriptionLimit = int64(1024 * 1024 * 50) // MaxFileSize is the maximum file size a user can upload -const MaxFileSize = int64(1024 * 1024 * 1024 * 5) +const MaxFileSize = int64(1024 * 1024 * 1024 * 10) // MaxUploadURLsLimit indicates the max number of upload urls which can be request in one go const MaxUploadURLsLimit = 50 @@ -415,12 +415,9 @@ func (c *FileController) getSignedURLForType(ctx *gin.Context, fileID int64, obj // ignore lint unused inspection func isCliRequest(ctx *gin.Context) bool { - // todo: (neeraj) remove this short-circuit after wasabi migration - return false // check if user-agent contains go-resty - //userAgent := ctx.Request.Header.Get("User-Agent") - //return strings.Contains(userAgent, "go-resty") - + userAgent := ctx.Request.Header.Get("User-Agent") + return strings.Contains(userAgent, "go-resty") } // getWasabiSignedUrlIfAvailable returns a signed URL for the given fileID and objectType. It prefers wasabi over b2 @@ -435,10 +432,6 @@ func (c *FileController) getWasabiSignedUrlIfAvailable(fileID int64, objType ent return c.getPreSignedURLForDC(s3Object.ObjectKey, dc) } } - // todo: (neeraj) remove this log after some time - log.WithFields(log.Fields{ - "fileID": fileID}).Info("File not found in wasabi, returning signed url from B2") - // return signed url from default hot bucket return c.getHotDcSignedUrl(s3Object.ObjectKey) } diff --git a/server/pkg/controller/filedata/controller.go b/server/pkg/controller/filedata/controller.go index 91645cc720..ec269b690c 100644 --- a/server/pkg/controller/filedata/controller.go +++ b/server/pkg/controller/filedata/controller.go @@ -2,8 +2,8 @@ package filedata import ( "context" - "database/sql" "errors" + "fmt" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/s3/s3manager" @@ -21,6 +21,7 @@ import ( "github.com/gin-contrib/requestid" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" + "net/http" "sync" gTime "time" ) @@ -96,6 +97,9 @@ func (c *Controller) InsertOrUpdateMetadata(ctx *gin.Context, req *fileData.PutF if req.Type != ente.MlData { return stacktrace.Propagate(ente.NewBadRequestWithMessage("unsupported object type "+string(req.Type)), "") } + if versionErr := c._validateLastUpdatedAt(ctx, req.LastUpdatedAt, req.FileID, req.Type); versionErr != nil { + return stacktrace.Propagate(versionErr, "") + } bucketID := c.S3Config.GetBucketID(req.Type) objectKey := fileData.ObjectMetadataKey(req.FileID, fileOwnerID, req.Type, nil) @@ -139,12 +143,12 @@ func (c *Controller) GetFileData(ctx *gin.Context, actorUser int64, req fileData } doRows, err := c.Repo.GetFilesData(ctx, req.Type, []int64{req.FileID}) if err != nil { - if errors.Is(err, sql.ErrNoRows) && req.PreferNoContent { - return nil, nil - } return nil, stacktrace.Propagate(err, "") } if len(doRows) == 0 || doRows[0].IsDeleted { + if req.PreferNoContent { + return nil, nil + } return nil, stacktrace.Propagate(&ente.ErrNotFoundError, "") } ctxLogger := log.WithFields(log.Fields{ @@ -162,6 +166,7 @@ func (c *Controller) GetFileData(ctx *gin.Context, actorUser int64, req fileData Type: doRows[0].Type, EncryptedData: s3MetaObject.EncryptedData, DecryptionHeader: s3MetaObject.DecryptionHeader, + UpdatedAt: doRows[0].UpdatedAt, }, nil } @@ -206,6 +211,7 @@ func (c *Controller) GetFilesData(ctx *gin.Context, req fileData.GetFilesData) ( Type: obj.dbEntry.Type, EncryptedData: obj.s3MetaObject.EncryptedData, DecryptionHeader: obj.s3MetaObject.DecryptionHeader, + UpdatedAt: obj.dbEntry.UpdatedAt, }) } } @@ -318,6 +324,38 @@ func (c *Controller) _checkMetadataReadOrWritePerm(ctx *gin.Context, userID int6 return nil } +func (c *Controller) _validateLastUpdatedAt(ctx *gin.Context, lastUpdatedAt *int64, fileID int64, oType ente.ObjectType) error { + if lastUpdatedAt == nil { + return nil + } + doRows, err := c.Repo.GetFilesData(ctx, oType, []int64{fileID}) + if err != nil { + return stacktrace.Propagate(err, "failed to get data") + } + var invalidVersionErr = &ente.ApiError{ + HttpStatusCode: http.StatusConflict, + Code: "INVALID_VERSION", + Message: "", + } + if len(doRows) == 0 { + if *lastUpdatedAt == 0 { + return nil + } + invalidVersionErr.Message = "non zero version empty data" + return invalidVersionErr + } + if doRows[0].IsDeleted { + invalidVersionErr.Message = "data deleted" + return invalidVersionErr + } + dbUpdatedAt := doRows[0].UpdatedAt + if dbUpdatedAt != *lastUpdatedAt { + invalidVersionErr.Message = fmt.Sprintf("version mismatch expected %d, found %d", dbUpdatedAt, *lastUpdatedAt) + return invalidVersionErr + } + return nil +} + // _checkPreviewWritePerm is func (c *Controller) _checkPreviewWritePerm(ctx *gin.Context, fileID int64, actorID int64) error { err := c.AccessCtrl.VerifyFileOwnership(ctx, &access.VerifyFileOwnershipParams{ diff --git a/server/pkg/controller/filedata/preview_files.go b/server/pkg/controller/filedata/preview_files.go index 50e5d65415..ee171e1b59 100644 --- a/server/pkg/controller/filedata/preview_files.go +++ b/server/pkg/controller/filedata/preview_files.go @@ -45,12 +45,23 @@ func (c *Controller) PreviewUploadURL(ctx *gin.Context, request filedata.Preview // note: instead of the final url, give a temp url for upload purpose. objectKey := filedata.ObjectKey(request.FileID, fileOwnerID, request.Type, &id) bucketID := c.S3Config.GetBucketID(request.Type) + if request.IsMultiPart { + multiPartUploadURLs, err2 := c.getMultiPartUploadURL(bucketID, objectKey, request.Count) + if err2 != nil { + return nil, stacktrace.Propagate(err2, "") + } + return &filedata.PreviewUploadUrl{ + ObjectID: id, + PartURLs: &multiPartUploadURLs.PartURLs, + CompleteURL: &multiPartUploadURLs.CompleteURL, + }, nil + } enteUrl, err := c.getUploadURL(bucketID, objectKey) if err != nil { return nil, stacktrace.Propagate(err, "") } return &filedata.PreviewUploadUrl{ ObjectID: id, - Url: enteUrl.URL, + Url: &enteUrl.URL, }, nil } diff --git a/server/pkg/controller/filedata/s3.go b/server/pkg/controller/filedata/s3.go index 80dde4b363..12ed0bc3d1 100644 --- a/server/pkg/controller/filedata/s3.go +++ b/server/pkg/controller/filedata/s3.go @@ -43,6 +43,48 @@ func (c *Controller) getUploadURL(dc string, objectKey string) (*ente.UploadURL, URL: url, }, nil } +func (c *Controller) getMultiPartUploadURL(dc string, objectKey string, count *int64) (*ente.MultipartUploadURLs, error) { + s3Client := c.S3Config.GetS3Client(dc) + bucket := c.S3Config.GetBucket(dc) + r, err := s3Client.CreateMultipartUpload(&s3.CreateMultipartUploadInput{ + Bucket: bucket, + Key: &objectKey, + }) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + err = c.ObjectCleanupController.AddMultipartTempObjectKey(objectKey, *r.UploadId, dc) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + multipartUploadURLs := ente.MultipartUploadURLs{ObjectKey: objectKey} + urls := make([]string, 0) + for i := int64(1); i <= *count; i++ { + partReq, _ := s3Client.UploadPartRequest(&s3.UploadPartInput{ + Bucket: bucket, + Key: &objectKey, + UploadId: r.UploadId, + PartNumber: &i, + }) + partUrl, partUrlErr := partReq.Presign(PreSignedRequestValidityDuration) + if partUrlErr != nil { + return nil, stacktrace.Propagate(partUrlErr, "") + } + urls = append(urls, partUrl) + } + multipartUploadURLs.PartURLs = urls + r2, _ := s3Client.CompleteMultipartUploadRequest(&s3.CompleteMultipartUploadInput{ + Bucket: bucket, + Key: &objectKey, + UploadId: r.UploadId, + }) + url, err := r2.Presign(PreSignedRequestValidityDuration) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + multipartUploadURLs.CompleteURL = url + return &multipartUploadURLs, nil +} func (c *Controller) signedUrlGet(dc string, objectKey string) (*ente.UploadURL, error) { s3Client := c.S3Config.GetS3Client(dc) diff --git a/server/pkg/controller/filedata/video.go b/server/pkg/controller/filedata/video.go index ee48c268db..8fba680e44 100644 --- a/server/pkg/controller/filedata/video.go +++ b/server/pkg/controller/filedata/video.go @@ -29,8 +29,7 @@ func (c *Controller) InsertVideoPreview(ctx *gin.Context, req *filedata.VidPrevi if sizeErr := c.verifySize(bucketID, fileObjectKey, req.ObjectSize); sizeErr != nil { return stacktrace.Propagate(sizeErr, "failed to validate size") } - // Start a goroutine to handle the upload and insert operations - //go func() { + obj := filedata.S3FileMetadata{ Version: *req.Version, EncryptedData: req.Playlist, @@ -59,9 +58,9 @@ func (c *Controller) InsertVideoPreview(ctx *gin.Context, req *filedata.VidPrevi dbInsertErr := c.Repo.InsertOrUpdatePreviewData(context.Background(), row, fileObjectKey) if dbInsertErr != nil { logger.WithError(dbInsertErr).Error("insert or update failed") - return nil + return stacktrace.Propagate(dbInsertErr, "failed to insert or update preview data") } - //}() + return nil } diff --git a/server/pkg/controller/stripe.go b/server/pkg/controller/stripe.go index a8ad9cfdcf..7e8b6ca837 100644 --- a/server/pkg/controller/stripe.go +++ b/server/pkg/controller/stripe.go @@ -17,6 +17,7 @@ import ( "github.com/ente-io/museum/ente" emailCtrl "github.com/ente-io/museum/pkg/controller/email" + timeUtil "github.com/ente-io/museum/pkg/utils/time" "github.com/ente-io/museum/pkg/repo" "github.com/ente-io/museum/pkg/utils/billing" "github.com/ente-io/museum/pkg/utils/email" @@ -233,11 +234,14 @@ func (c *StripeController) handleCheckoutSessionCompleted(event stripe.Event, co log.Warn("Webhook is reporting an outdated purchase that was already verified stripeSubscription:", stripeSubscription.ID) return ente.StripeEventLog{UserID: userID, StripeSubscription: stripeSubscription, Event: event}, nil } + isUpgradingFromFreePlan := currentSubscription.ProductID == ente.FreePlanProductID + if isUpgradingFromFreePlan { + newSubscription.UpgradedAt = timeUtil.Microseconds() + } err = c.BillingRepo.ReplaceSubscription( currentSubscription.ID, newSubscription, ) - isUpgradingFromFreePlan := currentSubscription.ProductID == ente.FreePlanProductID if isUpgradingFromFreePlan { go func() { cur := currency.MustParseISO(string(session.Currency)) diff --git a/server/pkg/middleware/rate_limit.go b/server/pkg/middleware/rate_limit.go index ed98805f04..14d3c92a00 100644 --- a/server/pkg/middleware/rate_limit.go +++ b/server/pkg/middleware/rate_limit.go @@ -24,6 +24,7 @@ type RateLimitMiddleware struct { reset time.Duration ticker *time.Ticker limit10ReqPerMin *limiter.Limiter + limit200ReqPerMin *limiter.Limiter limit200ReqPerSec *limiter.Limiter discordCtrl *discord.DiscordController } @@ -31,6 +32,7 @@ type RateLimitMiddleware struct { func NewRateLimitMiddleware(discordCtrl *discord.DiscordController, limit int64, reset time.Duration) *RateLimitMiddleware { rl := &RateLimitMiddleware{ limit10ReqPerMin: util.NewRateLimiter("10-M"), + limit200ReqPerMin: util.NewRateLimiter("200-M"), limit200ReqPerSec: util.NewRateLimiter("200-S"), discordCtrl: discordCtrl, limit: limit, @@ -131,10 +133,12 @@ func (r *RateLimitMiddleware) APIRateLimitForUserMiddleware(urlSanitizer func(_ // getLimiter, based on reqPath & reqMethod, return instance of limiter.Limiter which needs to // be applied for a request. It returns nil if the request is not rate limited func (r *RateLimitMiddleware) getLimiter(reqPath string, reqMethod string) *limiter.Limiter { + if reqPath == "/users/public-key" { + return r.limit200ReqPerMin + } if reqPath == "/users/ott" || reqPath == "/users/verify-email" || reqPath == "/user/change-email" || - reqPath == "/users/public-key" || reqPath == "/public-collection/verify-password" || reqPath == "/family/accept-invite" || reqPath == "/users/srp/attributes" || diff --git a/server/pkg/repo/billing.go b/server/pkg/repo/billing.go index d66dce90d7..18255c290b 100644 --- a/server/pkg/repo/billing.go +++ b/server/pkg/repo/billing.go @@ -69,9 +69,9 @@ func (repo *BillingRepository) UpdateTransactionIDOnDeletion(userID int64) error // ReplaceSubscription replaces a subscription with a new one func (repo *BillingRepository) ReplaceSubscription(subscriptionID int64, s ente.Subscription) error { _, err := repo.DB.Exec(`UPDATE subscriptions - SET storage = $2, original_transaction_id = $3, expiry_time = $4, product_id = $5, payment_provider = $6, attributes = $7 + SET storage = $2, original_transaction_id = $3, expiry_time = $4, product_id = $5, payment_provider = $6, attributes = $7, upgraded_at = $8 WHERE subscription_id = $1`, - subscriptionID, s.Storage, s.OriginalTransactionID, s.ExpiryTime, s.ProductID, s.PaymentProvider, s.Attributes) + subscriptionID, s.Storage, s.OriginalTransactionID, s.ExpiryTime, s.ProductID, s.PaymentProvider, s.Attributes, s.UpgradedAt) return stacktrace.Propagate(err, "") } @@ -155,7 +155,7 @@ func (repo *BillingRepository) LogStripePush(eventLog ente.StripeEventLog) error return stacktrace.Propagate(err, "") } -// LogStripePush logs a subscription modification by an admin +// LogAdminTriggeredSubscriptionUpdate logs a subscription modification by an admin func (repo *BillingRepository) LogAdminTriggeredSubscriptionUpdate(r ente.UpdateSubscriptionRequest) error { requestJSON, _ := json.Marshal(r) _, err := repo.DB.Exec(`INSERT INTO subscription_logs(user_id, payment_provider, notification, verification_response) VALUES($1, $2, $3, '{}'::json)`, diff --git a/server/pkg/repo/filedata/repository.go b/server/pkg/repo/filedata/repository.go index 9c16eb0efb..37afef9739 100644 --- a/server/pkg/repo/filedata/repository.go +++ b/server/pkg/repo/filedata/repository.go @@ -32,7 +32,7 @@ func (r *Repository) InsertOrUpdate(ctx context.Context, data filedata.Row) erro INSERT INTO file_data (file_id, user_id, data_type, size, latest_bucket, sync_locked_till) VALUES - ($1, $2, $3, $4, $5, now_utc_micro_seconds() + 5 * 60 * 1000*1000) + ($1, $2, $3, $4, $5, now_utc_micro_seconds() + 10 * 60 * 1000*1000) ON CONFLICT (file_id, data_type) DO UPDATE SET size = EXCLUDED.size, @@ -65,9 +65,9 @@ func (r *Repository) InsertOrUpdatePreviewData(ctx context.Context, data filedat } query := ` INSERT INTO file_data - (file_id, user_id, data_type, size, latest_bucket, obj_id, obj_nonce, obj_size ) + (file_id, user_id, data_type, size, latest_bucket, obj_id, obj_nonce, obj_size, sync_locked_till) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8) + ($1, $2, $3, $4, $5, $6, $7, $8, now_utc_micro_seconds() + 10 * 60 * 1000*1000) ON CONFLICT (file_id, data_type) DO UPDATE SET size = EXCLUDED.size, diff --git a/server/pkg/repo/passkey/passkey.go b/server/pkg/repo/passkey/passkey.go index 3910181a1d..6f51c2b144 100644 --- a/server/pkg/repo/passkey/passkey.go +++ b/server/pkg/repo/passkey/passkey.go @@ -13,8 +13,8 @@ import ( "github.com/ente-io/stacktrace" "github.com/go-webauthn/webauthn/protocol" "github.com/google/uuid" - "github.com/spf13/viper" "github.com/sirupsen/logrus" + "github.com/spf13/viper" "github.com/ente-io/museum/ente" "github.com/ente-io/museum/pkg/utils/byteMarshaller" @@ -144,7 +144,7 @@ func (r *Repository) CreateBeginRegistrationData(user *ente.User) (options *prot } if len(passkeyUser.WebAuthnCredentials()) >= ente.MaxPasskeys { - err = stacktrace.NewError(ente.ErrMaxPasskeysReached.Error()) + err = stacktrace.Propagate(&ente.ErrMaxPasskeysReached, "") return } diff --git a/server/pkg/repo/user.go b/server/pkg/repo/user.go index aa7d6145d5..bac3aaf2f1 100644 --- a/server/pkg/repo/user.go +++ b/server/pkg/repo/user.go @@ -332,6 +332,35 @@ func (repo *UserRepository) GetUsersWithIndividualPlanWhoHaveExceededStorageQuot return users, nil } +func (repo *UserRepository) GetUsersWhoUpgradedNDaysAgo(days int) ([]ente.User, error) { + rows, err := repo.DB.Query(` + SELECT u.user_id, u.encrypted_email, u.email_decryption_nonce, u.email_hash, u.creation_time + FROM users u + INNER JOIN subscriptions s ON u.user_id = s.user_id + WHERE s.upgraded_at >= $1 AND s.upgraded_at < $2 AND u.encrypted_email IS NOT NULL`, + time.MicrosecondsBeforeDays(days+1), time.MicrosecondsBeforeDays(days)) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + defer rows.Close() + users := make([]ente.User, 0) + for rows.Next() { + var user ente.User + var encryptedEmail, nonce []byte + err := rows.Scan(&user.ID, &encryptedEmail, &nonce, &user.Hash, &user.CreationTime) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + email, err := crypto.Decrypt(encryptedEmail, repo.SecretEncryptionKey, nonce) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + user.Email = email + users = append(users, user) + } + return users, nil +} + // SetTwoFactorSecret sets the two factor secret for a user func (repo *UserRepository) SetTwoFactorSecret(userID int64, secret ente.EncryptionResult, secretHash string, recoveryEncryptedTwoFactorSecret string, recoveryTwoFactorSecretDecryptionNonce string) error { _, err := repo.DB.Exec(`INSERT INTO two_factor(user_id,encrypted_two_factor_secret,two_factor_secret_decryption_nonce,two_factor_secret_hash,recovery_encrypted_two_factor_secret,recovery_two_factor_secret_decryption_nonce) diff --git a/server/pkg/utils/time/time.go b/server/pkg/utils/time/time.go index 69749a96a8..362deb5983 100644 --- a/server/pkg/utils/time/time.go +++ b/server/pkg/utils/time/time.go @@ -38,8 +38,8 @@ func MicrosecondsAfterDays(noOfDays int) int64 { return Microseconds() + int64(noOfDays*24)*MicroSecondsInOneHour } -// MicrosecondBeforeDays returns the time in micro seconds before noOfDays -func MicrosecondBeforeDays(noOfDays int) int64 { +// MicrosecondsBeforeDays returns the time in micro seconds before noOfDays +func MicrosecondsBeforeDays(noOfDays int) int64 { return Microseconds() - int64(noOfDays*24)*MicroSecondsInOneHour } diff --git a/web/apps/accounts/src/pages/passkeys/index.tsx b/web/apps/accounts/src/pages/passkeys/index.tsx index 671ea54876..5aab316ffa 100644 --- a/web/apps/accounts/src/pages/passkeys/index.tsx +++ b/web/apps/accounts/src/pages/passkeys/index.tsx @@ -3,9 +3,20 @@ import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; import KeyIcon from "@mui/icons-material/Key"; -import { Box, Paper, Stack, Typography, styled } from "@mui/material"; +import { + Box, + Paper, + Stack, + TextField, + Typography, + styled, +} from "@mui/material"; import { EnteLogo } from "ente-base/components/EnteLogo"; -import { SidebarDrawer } from "ente-base/components/mui/SidebarDrawer"; +import { LoadingButton } from "ente-base/components/mui/LoadingButton"; +import { + SidebarDrawer, + SidebarDrawerTitlebar, +} from "ente-base/components/mui/SidebarDrawer"; import { NavbarBase } from "ente-base/components/Navbar"; import { RowButton, @@ -13,13 +24,12 @@ import { RowButtonGroup, } from "ente-base/components/RowButton"; import { SingleInputDialog } from "ente-base/components/SingleInputDialog"; -import { Titlebar } from "ente-base/components/Titlebar"; import { errorDialogAttributes } from "ente-base/components/utils/dialog"; import { useModalVisibility } from "ente-base/components/utils/modal"; import { useBaseContext } from "ente-base/context"; import { formattedDateTime } from "ente-base/i18n-date"; import log from "ente-base/log"; -import SingleInputForm from "ente-shared/components/SingleInputForm"; +import { useFormik } from "formik"; import { t } from "i18next"; import React, { useCallback, useEffect, useState } from "react"; import { @@ -91,30 +101,6 @@ const Page: React.FC = () => { void refreshPasskeys(); }; - const handleSubmit = async ( - inputValue: string, - setFieldError: (errorMessage: string) => void, - resetForm: () => void, - ) => { - try { - await registerPasskey(token!, inputValue); - } catch (e) { - log.error("Failed to register a new passkey", e); - // If the user cancels the operation, then an error with name - // "NotAllowedError" is thrown. - // - // Ignore these, but in other cases add an error indicator to the - // add passkey text field. The browser is expected to already have - // shown an error dialog to the user. - if (!(e instanceof Error && e.name == "NotAllowedError")) { - setFieldError(t("passkey_add_failed")); - } - return; - } - await refreshPasskeys(); - resetForm(); - }; - return ( @@ -124,14 +110,10 @@ const Page: React.FC = () => { sx={{ alignSelf: "center", m: 3, maxWidth: "375px", gap: 3 }} > {t("passkeys_description")} - - + { export default Page; +interface AddPasskeyFormProps { + /** + * The token to use for the API request for adding the passkey. + */ + token: string; + /** + * Called to refresh the list of passkeys shown on the page after the passkey was successfully added. + */ + onRefreshPasskeys: () => Promise; +} + +export const AddPasskeyForm: React.FC = ({ + token, + onRefreshPasskeys, +}) => { + const formik = useFormik({ + initialValues: { value: "" }, + onSubmit: async (values, { setFieldError, resetForm }) => { + const value = values.value; + const setValueFieldError = (message: string) => + setFieldError("value", message); + + if (!value) { + setValueFieldError(t("required")); + return; + } + + try { + await registerPasskey(token, value); + } catch (e) { + log.error("Failed to register a new passkey", e); + // If the user cancels the operation, then an error with name + // "NotAllowedError" is thrown. + // + // Ignore these, but in other cases add an error indicator to the + // add passkey text field. The browser is expected to already have + // shown an error dialog to the user. + if (!(e instanceof Error && e.name == "NotAllowedError")) { + setValueFieldError(t("passkey_add_failed")); + } + return; + } + await onRefreshPasskeys(); + resetForm(); + }, + }); + + return ( +

+ + + {t("add_passkey")} + + + ); +}; + interface PasskeysListProps { /** The list of {@link Passkey}s to show. */ passkeys: Passkey[]; @@ -287,7 +343,7 @@ const ManagePasskeyDrawer: React.FC = ({ {token && passkey && ( - = ({ {formattedDateTime(passkey.createdAt)} - + } label={t("rename_passkey")} diff --git a/web/apps/auth/public/_headers b/web/apps/auth/public/_headers index 27d91b010a..5672a7a690 100644 --- a/web/apps/auth/public/_headers +++ b/web/apps/auth/public/_headers @@ -5,5 +5,6 @@ X-Download-Options: noopen X-Frame-Options: deny X-XSS-Protection: 1; mode=block - Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' blob: data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src 'self'; child-src 'self' blob:; object-src 'none'; connect-src 'self' https://*.ente.io data: blob:; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io; + Reporting-Endpoints: csp-endpoint="https://csp-reporter.ente.io" + Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' blob: data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src 'self'; child-src 'self' blob:; object-src 'none'; connect-src 'self' https://*.ente.io data: blob:; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to csp-endpoint; diff --git a/web/apps/auth/src/pages/auth.tsx b/web/apps/auth/src/pages/auth.tsx index 485511edba..9c954ab564 100644 --- a/web/apps/auth/src/pages/auth.tsx +++ b/web/apps/auth/src/pages/auth.tsx @@ -27,13 +27,15 @@ import { t } from "i18next"; import { useRouter } from "next/router"; import React, { useCallback, useEffect, useState } from "react"; import { generateOTPs, type Code } from "services/code"; -import { getAuthCodes } from "services/remote"; +import { getAuthCodesAndTimeOffset } from "services/remote"; +import { prettyFormatCode } from "utils/format"; const Page: React.FC = () => { const { logout, showMiniDialog } = useBaseContext(); const router = useRouter(); const [codes, setCodes] = useState([]); + const [timeOffset, setTimeOffset] = useState(0); const [hasFetched, setHasFetched] = useState(false); const [searchTerm, setSearchTerm] = useState(""); @@ -47,7 +49,10 @@ const Page: React.FC = () => { } try { - setCodes(await getAuthCodes(masterKey)); + const { codes, timeOffset } = + await getAuthCodesAndTimeOffset(masterKey); + setCodes(codes); + setTimeOffset(timeOffset ?? 0); } catch (e) { log.error("Failed to fetch codes", e); if (isHTTP401Error(e)) @@ -117,7 +122,10 @@ const Page: React.FC = () => { ) : ( filteredCodes.map((code) => ( - + )) )} @@ -162,9 +170,10 @@ const AuthNavbar: React.FC = () => { interface CodeDisplayProps { code: Code; + timeOffset: number; } -const CodeDisplay: React.FC = ({ code }) => { +const CodeDisplay: React.FC = ({ code, timeOffset }) => { const [otp, setOTP] = useState(""); const [nextOTP, setNextOTP] = useState(""); const [errorMessage, setErrorMessage] = useState(""); @@ -172,13 +181,13 @@ const CodeDisplay: React.FC = ({ code }) => { const regen = useCallback(() => { try { - const [m, n] = generateOTPs(code); + const [m, n] = generateOTPs(code, timeOffset); setOTP(m); setNextOTP(n); } catch (e) { setErrorMessage(e instanceof Error ? e.message : String(e)); } - }, [code]); + }, [code, timeOffset]); const copyCode = () => void navigator.clipboard.writeText(otp).then(() => { @@ -191,7 +200,8 @@ const CodeDisplay: React.FC = ({ code }) => { regen(); const periodMs = code.period * 1000; - const timeToNextCode = periodMs - (Date.now() % periodMs); + const timeToNextCode = + periodMs - ((Date.now() + timeOffset) % periodMs); let interval: ReturnType | undefined; // Wait until we are at the start of the next code period, and then @@ -204,7 +214,7 @@ const CodeDisplay: React.FC = ({ code }) => { }, timeToNextCode); return () => interval && clearInterval(interval); - }, [code, regen]); + }, [code, timeOffset, regen]); return ( @@ -212,7 +222,11 @@ const CodeDisplay: React.FC = ({ code }) => { ) : ( - + = ({ code }) => { ); }; -interface OTPDisplayProps { - code: Code; - otp: string; - nextOTP: string; -} +type OTPDisplayProps = CodeValidityBarProps & { otp: string; nextOTP: string }; -const OTPDisplay: React.FC = ({ code, otp, nextOTP }) => { +const OTPDisplay: React.FC = ({ + code, + timeOffset, + otp, + nextOTP, +}) => { return ( ({ @@ -247,7 +262,7 @@ const OTPDisplay: React.FC = ({ code, otp, nextOTP }) => { overflow: "hidden", })} > - + = ({ code, otp, nextOTP }) => { interface CodeValidityBarProps { code: Code; + timeOffset: number; } -const CodeValidityBar: React.FC = ({ code }) => { +const CodeValidityBar: React.FC = ({ + code, + timeOffset, +}) => { const theme = useTheme(); const [progress, setProgress] = useState(code.type == "hotp" ? 1 : 0); useEffect(() => { const advance = () => { const us = code.period * 1e6; - const timeRemaining = us - ((Date.now() * 1000) % us); + const timeRemaining = + us - (((Date.now() + timeOffset) * 1000) % us); setProgress(timeRemaining / us); }; @@ -312,7 +332,7 @@ const CodeValidityBar: React.FC = ({ code }) => { code.type == "hotp" ? undefined : setInterval(advance, 10); return () => ticker && clearInterval(ticker); - }, [code]); + }, [code, timeOffset]); const progressColor = progress > 0.4 diff --git a/web/apps/auth/src/pages/share.tsx b/web/apps/auth/src/pages/share.tsx index a86d9411d2..40145531e5 100644 --- a/web/apps/auth/src/pages/share.tsx +++ b/web/apps/auth/src/pages/share.tsx @@ -2,6 +2,7 @@ import { Box, Button, Stack, Typography, useTheme } from "@mui/material"; import { EnteLogo } from "ente-base/components/EnteLogo"; import { decryptMetadataJSON_New } from "ente-base/crypto"; import React, { useEffect, useMemo, useState } from "react"; +import { prettyFormatCode } from "utils/format"; interface SharedCode { startTime: number; @@ -249,14 +250,12 @@ const parseCodeDisplay = ( const progress = ((elapsedTime % stepDuration) / stepDuration) * 100; return { - currentCode: formatCode(codes[index] ?? ""), - nextCode: formatCode(codes[index + 1] ?? ""), + currentCode: prettyFormatCode(codes[index] ?? ""), + nextCode: prettyFormatCode(codes[index + 1] ?? ""), progress, }; }; -const formatCode = (code: string) => code.replace(/(.{3})/g, "$1 ").trim(); - const Message: React.FC = ({ children }) => ( {children} diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts index c312285705..2d557dc549 100644 --- a/web/apps/auth/src/services/code.ts +++ b/web/apps/auth/src/services/code.ts @@ -256,12 +256,19 @@ const parseCodeDisplay = (url: URL): CodeDisplay | undefined => { * * @param code The parsed code data, including the secret and code type. * + * @param timeOffset A millisecond delta that should be applied to Date.now when + * deriving the OTP. + * * @returns a pair of OTPs, the current one and the next one, using the given * {@link code}. */ -export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => { +export const generateOTPs = ( + code: Code, + timeOffset: number, +): [otp: string, nextOTP: string] => { let otp: string; let nextOTP: string; + const timestamp = Date.now() + timeOffset; switch (code.type) { case "totp": { const totp = new TOTP({ @@ -270,9 +277,9 @@ export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => { period: code.period, digits: code.length, }); - otp = totp.generate(); + otp = totp.generate({ timestamp }); nextOTP = totp.generate({ - timestamp: Date.now() + code.period * 1000, + timestamp: timestamp + code.period * 1000, }); break; } @@ -291,9 +298,9 @@ export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => { case "steam": { const steam = new Steam({ secret: code.secret }); - otp = steam.generate(); + otp = steam.generate({ timestamp }); nextOTP = steam.generate({ - timestamp: Date.now() + code.period * 1000, + timestamp: timestamp + code.period * 1000, }); break; } diff --git a/web/apps/auth/src/services/remote.ts b/web/apps/auth/src/services/remote.ts index eccf7ab402..35ed191b8c 100644 --- a/web/apps/auth/src/services/remote.ts +++ b/web/apps/auth/src/services/remote.ts @@ -7,21 +7,37 @@ import { import log from "ente-base/log"; import { apiURL } from "ente-base/origins"; import { ensureString } from "ente-utils/ensure"; +import { nullToUndefined } from "ente-utils/transform"; import { codeFromURIString, type Code } from "services/code"; import { z } from "zod"; -export const getAuthCodes = async (masterKey: Uint8Array): Promise => { +export interface AuthCodesAndTimeOffset { + codes: Code[]; + /** + * An optional and approximate correction (milliseconds) which should be + * applied to the current client's time when deriving TOTPs. + */ + timeOffset?: number; +} + +export const getAuthCodesAndTimeOffset = async ( + masterKey: Uint8Array, +): Promise => { const authenticatorEntityKey = await getAuthenticatorEntityKey(); if (!authenticatorEntityKey) { // The user might not have stored any codes yet from the mobile app. - return []; + return { codes: [] }; } const authenticatorKey = await decryptAuthenticatorKey( authenticatorEntityKey, masterKey, ); - return (await authenticatorEntityDiff(authenticatorKey)) + + const { entities, timeOffset } = + await authenticatorEntityDiff(authenticatorKey); + + const codes = entities .map((entity) => { try { return codeFromURIString(entity.id, ensureString(entity.data)); @@ -53,6 +69,8 @@ export const getAuthCodes = async (masterKey: Uint8Array): Promise => { } return 0; }); + + return { codes, timeOffset }; }; /** @@ -93,15 +111,50 @@ const RemoteAuthenticatorEntityChange = z.object({ updatedAt: z.number(), }); +const AuthenticatorEntityDiffResponse = z.object({ + /** + * Changes to entities. + */ + diff: z.array(RemoteAuthenticatorEntityChange), + /** + * An optional epoch microseconds indicating the remote time when it + * generated the response. + */ + timestamp: z.number().nullish().transform(nullToUndefined), +}); + +export interface AuthenticatorEntityDiffResult { + /** + * The decrypted {@link AuthenticatorEntity} values. + */ + entities: AuthenticatorEntity[]; + /** + * An optional and approximate offset (in milliseconds) by which the time on + * the current client is out of sync. + * + * This offset is computed by calculated by comparing the timestamp when the + * remote generated the response to the time we received it. As such + * (because of network delays etc) this will not be an accurate offset, + * neither is it meant to be - it is only meant to help users whose devices + * have wildly off times to still see the correct codes. + * + * Note that, for various reasons, remote might not send us a timestamp when + * fetching the diff, so this is a best effort correction, and is not + * guaranteed to be present. + */ + timeOffset: number | undefined; +} + /** - * Fetch all the authenticator entities for the user. + * Fetch all the authenticator entities for the user, and an estimated time + * drift for the current client. * * @param authenticatorKey The (base64 encoded) key that should be used for * decrypting the authenticator entities received from remote. */ export const authenticatorEntityDiff = async ( authenticatorKey: string, -): Promise => { +): Promise => { const decrypt = (encryptedData: string, decryptionHeader: string) => decryptMetadataJSON_New( { encryptedData, decryptionHeader }, @@ -109,13 +162,15 @@ export const authenticatorEntityDiff = async ( ); // Fetch all the entities, paginating the requests. - const entities = new Map< + const encryptedEntities = new Map< string, { id: string; encryptedData: string; header: string } >(); let sinceTime = 0; const batchSize = 2500; + let timeOffset: number | undefined = undefined; + while (true) { const params = new URLSearchParams({ sinceTime: `${sinceTime}`, @@ -126,17 +181,25 @@ export const authenticatorEntityDiff = async ( headers: await authenticatedRequestHeaders(), }); ensureOk(res); - const diff = z - .object({ diff: z.array(RemoteAuthenticatorEntityChange) }) - .parse(await res.json()).diff; + + const { diff, timestamp } = AuthenticatorEntityDiffResponse.parse( + await res.json(), + ); + + if (timestamp) { + // - timestamp is in epoch microseconds. + // - Date.now and timeOffset are in epoch milliseconds. + timeOffset = Date.now() - Math.floor(timestamp / 1e3); + } + if (diff.length == 0) break; for (const change of diff) { sinceTime = Math.max(sinceTime, change.updatedAt); if (change.isDeleted) { - entities.delete(change.id); + encryptedEntities.delete(change.id); } else { - entities.set(change.id, { + encryptedEntities.set(change.id, { id: change.id, encryptedData: change.encryptedData!, header: change.header!, @@ -145,12 +208,16 @@ export const authenticatorEntityDiff = async ( } } - return Promise.all( - [...entities.values()].map(async ({ id, encryptedData, header }) => ({ - id, - data: await decrypt(encryptedData, header), - })), + const entities = await Promise.all( + [...encryptedEntities.values()].map( + async ({ id, encryptedData, header }) => ({ + id, + data: await decrypt(encryptedData, header), + }), + ), ); + + return { entities, timeOffset }; }; export const AuthenticatorEntityKey = z.object({ diff --git a/web/apps/auth/src/utils/format.ts b/web/apps/auth/src/utils/format.ts new file mode 100644 index 0000000000..169698c9fb --- /dev/null +++ b/web/apps/auth/src/utils/format.ts @@ -0,0 +1,6 @@ +/** + * Format a generated OTP code to improve readability by breaking it into chunks + * of length 3. + */ +export const prettyFormatCode = (code: string) => + code.replace(/(.{3})/g, "$1 ").trim(); diff --git a/web/apps/payments/package.json b/web/apps/payments/package.json index 87a6b3fe3e..96d8ea8204 100644 --- a/web/apps/payments/package.json +++ b/web/apps/payments/package.json @@ -9,15 +9,15 @@ "preview": "vite preview --port 3001" }, "dependencies": { - "@stripe/stripe-js": "^1.17.0", + "@stripe/stripe-js": "1.54.2", "react": "^19.1.0", "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.3", - "@vitejs/plugin-react": "^4.4.1", + "@types/react": "^19.1.5", + "@types/react-dom": "^19.1.5", + "@vitejs/plugin-react": "^4.5.0", "ente-build-config": "*", - "vite": "^6.3.4" + "vite": "^6.3.5" } } diff --git a/web/apps/photos/package.json b/web/apps/photos/package.json index 125b13e905..b4a894dd41 100644 --- a/web/apps/photos/package.json +++ b/web/apps/photos/package.json @@ -22,13 +22,12 @@ "react-virtualized-auto-sizer": "^1.0.26", "react-window": "^1.8.11", "sanitize-filename": "^1.6.3", - "similarity-transformation": "^0.0.1", - "xml-js": "^1.6.11" + "similarity-transformation": "^0.0.1" }, "devDependencies": { - "@types/node": "^22.15.3", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.3", + "@types/node": "^22.15.21", + "@types/react": "^19.1.5", + "@types/react-dom": "^19.1.5", "@types/react-window": "^1.8.8", "ente-build-config": "*" } diff --git a/web/apps/photos/public/_headers b/web/apps/photos/public/_headers index 86b996a357..1af254adb5 100644 --- a/web/apps/photos/public/_headers +++ b/web/apps/photos/public/_headers @@ -5,5 +5,6 @@ X-Download-Options: noopen X-Frame-Options: deny X-XSS-Protection: 1; mode=block - Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' blob: data: https://*.openstreetmap.org; media-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' https://assets.ente.io 'unsafe-eval' blob:; manifest-src 'self'; child-src 'self' blob:; object-src 'none'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com https://ente-prod-v3.s3.eu-central-2.wasabisys.com; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io; + Reporting-Endpoints: csp-endpoint="https://csp-reporter.ente.io" + Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' blob: data: https://*.openstreetmap.org; media-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' https://assets.ente.io 'unsafe-eval' blob:; manifest-src 'self'; child-src 'self' blob:; object-src 'none'; connect-src 'self' https://*.ente.io data: blob: https://*.s3.eu-central-003.backblazeb2.com https://*.s3.eu-central-2.wasabisys.com; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to csp-endpoint; diff --git a/web/apps/photos/src/components/Collections/AlbumCastDialog.tsx b/web/apps/photos/src/components/Collections/AlbumCastDialog.tsx index 36a4d953cc..fd36f14a12 100644 --- a/web/apps/photos/src/components/Collections/AlbumCastDialog.tsx +++ b/web/apps/photos/src/components/Collections/AlbumCastDialog.tsx @@ -2,6 +2,12 @@ import { Link, Stack, Typography } from "@mui/material"; import { TitledMiniDialog } from "ente-base/components/MiniDialog"; import { ActivityIndicator } from "ente-base/components/mui/ActivityIndicator"; import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; +import { + SingleInputForm, + type SingleInputFormProps, +} from "ente-base/components/SingleInputForm"; +import type { ModalVisibilityProps } from "ente-base/components/utils/modal"; +import { ut } from "ente-base/i18n"; import log from "ente-base/log"; import type { Collection } from "ente-media/collection"; import { useSettingsSnapshot } from "ente-new/photos/components/utils/use-snapshot"; @@ -11,21 +17,14 @@ import { unknownDeviceCodeErrorMessage, } from "ente-new/photos/services/cast"; import { loadCast } from "ente-new/photos/utils/chromecast-sender"; -import SingleInputForm, { - type SingleInputFormProps, -} from "ente-shared/components/SingleInputForm"; import { t } from "i18next"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { Trans } from "react-i18next"; -interface AlbumCastDialogProps { - /** If `true`, the dialog is shown. */ - open: boolean; - /** Callback fired when the dialog wants to be closed. */ - onClose: () => void; +type AlbumCastDialogProps = ModalVisibilityProps & { /** The collection that we want to cast. */ collection: Collection; -} +}; /** * A dialog that shows various options that the user has for casting an album. @@ -56,25 +55,25 @@ export const AlbumCastDialog: React.FC = ({ setBrowserCanCast(typeof window["chrome"] != "undefined"); }, []); - const onSubmit: SingleInputFormProps["callback"] = async ( - value, - setFieldError, - ) => { - try { - await publishCastPayload(value.trim(), collection); - onClose(); - } catch (e) { - log.error("Failed to cast", e); - if ( - e instanceof Error && - e.message == unknownDeviceCodeErrorMessage - ) { - setFieldError(t("tv_not_found")); - } else { - setFieldError(t("generic_error_retry")); + const onSubmit: SingleInputFormProps["onSubmit"] = useCallback( + async (value, setFieldError) => { + try { + await publishCastPayload(value.trim(), collection); + onClose(); + } catch (e) { + log.error("Failed to cast", e); + if ( + e instanceof Error && + e.message == unknownDeviceCodeErrorMessage + ) { + setFieldError(t("tv_not_found")); + } else { + throw e; + } } - } - }; + }, + [onClose, collection], + ); useEffect(() => { if (view == "auto") { @@ -195,17 +194,17 @@ export const AlbumCastDialog: React.FC = ({ setView("choose")} + sx={{ mt: 1 }} > {t("go_back")} diff --git a/web/apps/photos/src/components/Collections/AllAlbums.tsx b/web/apps/photos/src/components/Collections/AllAlbums.tsx index c9bcf6993f..9406779c0b 100644 --- a/web/apps/photos/src/components/Collections/AllAlbums.tsx +++ b/web/apps/photos/src/components/Collections/AllAlbums.tsx @@ -20,7 +20,6 @@ import { } from "ente-new/photos/components/Tiles"; import type { CollectionSummary } from "ente-new/photos/services/collection/ui"; import { CollectionsSortBy } from "ente-new/photos/services/collection/ui"; -import { FlexWrapper, FluidContainer } from "ente-shared/components/Container"; import { t } from "i18next"; import memoize from "memoize-one"; import React, { useEffect, useRef, useState } from "react"; @@ -104,8 +103,8 @@ const Title = ({ isInHiddenSection, }) => ( - - + + {isInHiddenSection @@ -123,18 +122,16 @@ const Title = ({ {t("albums_count", { count: collectionCount })} - - - - - - - + + + + + ); @@ -170,7 +167,7 @@ const AlbumsRow = React.memo( const collectionRow = collectionRowList[index]; return (
- + {collectionRow.map((item: any) => ( ))} - +
); }, diff --git a/web/apps/photos/src/components/Collections/CollectionHeader.tsx b/web/apps/photos/src/components/Collections/CollectionHeader.tsx index 224a76b713..35fcb89986 100644 --- a/web/apps/photos/src/components/Collections/CollectionHeader.tsx +++ b/web/apps/photos/src/components/Collections/CollectionHeader.tsx @@ -15,7 +15,6 @@ import UnarchiveIcon from "@mui/icons-material/Unarchive"; import VisibilityOffOutlinedIcon from "@mui/icons-material/VisibilityOffOutlined"; import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined"; import { Box, IconButton, Menu, Stack, Tooltip } from "@mui/material"; -import { SetCollectionNamerAttributes } from "components/Collections/CollectionNamer"; import { assertionFailed } from "ente-base/assert"; import { SpacedRow } from "ente-base/components/containers"; import { ActivityIndicator } from "ente-base/components/mui/ActivityIndicator"; @@ -23,6 +22,7 @@ import { OverflowMenu, OverflowMenuOption, } from "ente-base/components/OverflowMenu"; +import { SingleInputDialog } from "ente-base/components/SingleInputDialog"; import { useModalVisibility } from "ente-base/components/utils/modal"; import { useBaseContext } from "ente-base/context"; import { @@ -70,7 +70,6 @@ interface CollectionHeaderProps { isActiveCollectionDownloadInProgress: () => boolean; onCollectionShare: () => void; onCollectionCast: () => void; - setCollectionNamerAttributes: SetCollectionNamerAttributes; setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator; } @@ -128,7 +127,6 @@ const CollectionOptions: React.FC = ({ setActiveCollectionID, onCollectionShare, onCollectionCast, - setCollectionNamerAttributes, setFilesDownloadProgressAttributesCreator, isActiveCollectionDownloadInProgress, }) => { @@ -139,6 +137,8 @@ const CollectionOptions: React.FC = ({ const { show: showSortOrderMenu, props: sortOrderMenuVisibilityProps } = useModalVisibility(); + const { show: showAlbumNameInput, props: albumNameInputVisibilityProps } = + useModalVisibility(); const { type: collectionSummaryType } = collectionSummary; @@ -165,23 +165,15 @@ const CollectionOptions: React.FC = ({ [showLoadingBar, hideLoadingBar, onGenericError, syncWithRemote], ); - const showRenameCollectionModal = () => { - setCollectionNamerAttributes({ - title: t("rename_album"), - buttonText: t("rename"), - autoFilledName: activeCollection.name, - callback: renameCollection, - }); - }; - - const _renameCollection = async (newName: string) => { - if (activeCollection.name !== newName) { - await CollectionAPI.renameCollection(activeCollection, newName); - } - }; - - const renameCollection = (newName: string) => - wrap(() => _renameCollection(newName))(); + const renameCollection = useCallback( + async (newName: string) => { + if (activeCollection.name !== newName) { + await CollectionAPI.renameCollection(activeCollection, newName); + void syncWithRemote(false, true); + } + }, + [activeCollection], + ); const confirmDeleteCollection = () => { showMiniDialog({ @@ -313,6 +305,195 @@ const CollectionOptions: React.FC = ({ changeCollectionSortOrder(activeCollection, false), ); + let menuOptions: React.ReactNode[] = []; + // MUI doesn't let us use fragments to pass multiple menu items, so we need + // to instead put them in an array. This also necessitates giving each a + // unique key. + switch (collectionSummaryType) { + case "trash": + menuOptions = [ + , + ]; + break; + + case "favorites": + menuOptions = [ + + {t("download_favorites")} + , + } + > + {t("share_favorites")} + , + } + onClick={onCollectionCast} + > + {t("cast_to_tv")} + , + ]; + break; + + case "uncategorized": + menuOptions = [ + + {t("download_uncategorized")} + , + ]; + break; + + case "hiddenItems": + menuOptions = [ + + {t("download_hidden_items")} + , + ]; + break; + + case "incomingShareViewer": + case "incomingShareCollaborator": + menuOptions = [ + isArchivedCollection(activeCollection) ? ( + } + > + {t("unarchive_album")} + + ) : ( + } + > + {t("archive_album")} + + ), + } + onClick={confirmLeaveSharedAlbum} + > + {t("leave_album")} + , + } + onClick={onCollectionCast} + > + {t("cast_album_to_tv")} + , + ]; + break; + + default: + menuOptions = [ + } + > + {t("rename_album")} + , + } + > + {t("sort_by")} + , + isPinnedCollection(activeCollection) ? ( + } + > + {t("unpin_album")} + + ) : ( + } + > + {t("pin_album")} + + ), + ...(!isHiddenCollection(activeCollection) + ? [ + isArchivedCollection(activeCollection) ? ( + } + > + {t("unarchive_album")} + + ) : ( + } + > + {t("archive_album")} + + ), + ] + : []), + isHiddenCollection(activeCollection) ? ( + } + > + {t("unhide_collection")} + + ) : ( + } + > + {t("hide_collection")} + + ), + } + onClick={confirmDeleteCollection} + > + {t("delete_album")} + , + } + > + {t("share_album")} + , + } + onClick={onCollectionCast} + > + {t("cast_album_to_tv")} + , + ]; + break; + } + return ( = ({ ariaID="collection-options" triggerButtonIcon={} > - {collectionSummaryType == "trash" ? ( - - ) : collectionSummaryType == "favorites" ? ( - - {t("download_favorites")} - - ) : collectionSummaryType == "uncategorized" ? ( - - {t("download_uncategorized")} - - ) : collectionSummaryType == "hiddenItems" ? ( - - {t("download_hidden_items")} - - ) : collectionSummaryType == "incomingShareViewer" || - collectionSummaryType == "incomingShareCollaborator" ? ( - - ) : ( - - )} + {...menuOptions} = ({ onAscClick={changeSortOrderAsc} onDescClick={changeSortOrderDesc} /> + ); }; @@ -438,9 +583,9 @@ const EmptyTrashQuickOption: React.FC = ({ onClick }) => ( ); const showDownloadQuickOption = (type: CollectionSummaryType) => + type == "album" || type == "folder" || type == "favorites" || - type == "album" || type == "uncategorized" || type == "hiddenItems" || type == "incomingShareViewer" || @@ -476,8 +621,9 @@ const DownloadQuickOption: React.FC = ({ ); const showShareQuickOption = (type: CollectionSummaryType) => - type == "folder" || type == "album" || + type == "folder" || + type == "favorites" || type == "outgoingShare" || type == "sharedOnlyViaLink" || type == "archived" || @@ -502,7 +648,9 @@ const ShareQuickOption: React.FC = ({ : collectionSummaryType == "outgoingShare" || collectionSummaryType == "sharedOnlyViaLink" ? t("modify_sharing") - : t("share_album") + : collectionSummaryType == "favorites" + ? t("share_favorites") + : t("share_album") } > @@ -542,153 +690,6 @@ const DownloadOption: React.FC< ); -interface SharedCollectionOptionProps { - isArchived: boolean; - onArchiveClick: () => void; - onUnarchiveClick: () => void; - onLeaveSharedAlbumClick: () => void; - onCastClick: () => void; -} - -const SharedCollectionOptions: React.FC = ({ - isArchived, - onArchiveClick, - onUnarchiveClick, - onLeaveSharedAlbumClick, - onCastClick, -}) => ( - <> - {isArchived ? ( - } - > - {t("unarchive_album")} - - ) : ( - } - > - {t("archive_album")} - - )} - } - onClick={onLeaveSharedAlbumClick} - > - {t("leave_album")} - - } onClick={onCastClick}> - {t("cast_album_to_tv")} - - -); - -interface AlbumCollectionOptionsProps { - isArchived: boolean; - isPinned: boolean; - isHidden: boolean; - onRenameClick: () => void; - onSortClick: () => void; - onArchiveClick: () => void; - onUnarchiveClick: () => void; - onPinClick: () => void; - onUnpinClick: () => void; - onHideClick: () => void; - onUnhideClick: () => void; - onDeleteClick: () => void; - onShareClick: () => void; - onCastClick: () => void; -} - -const AlbumCollectionOptions: React.FC = ({ - isArchived, - isPinned, - isHidden, - onRenameClick, - onSortClick, - onArchiveClick, - onUnarchiveClick, - onPinClick, - onUnpinClick, - onHideClick, - onUnhideClick, - onDeleteClick, - onShareClick, - onCastClick, -}) => ( - <> - }> - {t("rename_album")} - - }> - {t("sort_by")} - - {isPinned ? ( - } - > - {t("unpin_album")} - - ) : ( - } - > - {t("pin_album")} - - )} - {!isHidden && ( - <> - {isArchived ? ( - } - > - {t("unarchive_album")} - - ) : ( - } - > - {t("archive_album")} - - )} - - )} - {isHidden ? ( - } - > - {t("unhide_collection")} - - ) : ( - } - > - {t("hide_collection")} - - )} - } - onClick={onDeleteClick} - > - {t("delete_album")} - - }> - {t("share_album")} - - } onClick={onCastClick}> - {t("cast_album_to_tv")} - - -); - interface CollectionSortOrderMenuProps { open: boolean; onClose: () => void; diff --git a/web/apps/photos/src/components/Collections/CollectionNamer.tsx b/web/apps/photos/src/components/Collections/CollectionNamer.tsx deleted file mode 100644 index e3967fa304..0000000000 --- a/web/apps/photos/src/components/Collections/CollectionNamer.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { TitledMiniDialog } from "ente-base/components/MiniDialog"; -import log from "ente-base/log"; -import SingleInputForm, { - type SingleInputFormProps, -} from "ente-shared/components/SingleInputForm"; -import { t } from "i18next"; -import React from "react"; - -export interface CollectionNamerAttributes { - callback: (name: string) => void; - title: string; - autoFilledName: string; - buttonText: string; -} - -export type SetCollectionNamerAttributes = React.Dispatch< - React.SetStateAction ->; - -interface Props { - show: boolean; - onHide: () => void; - attributes: CollectionNamerAttributes; -} - -export default function CollectionNamer({ attributes, ...props }: Props) { - if (!attributes) { - return <>; - } - const onSubmit: SingleInputFormProps["callback"] = async ( - albumName, - setFieldError, - ) => { - try { - attributes.callback(albumName); - props.onHide(); - } catch (e) { - log.error(e); - setFieldError(t("generic_error_retry")); - } - }; - - return ( - - - - ); -} diff --git a/web/apps/photos/src/components/Collections/CollectionShare.tsx b/web/apps/photos/src/components/Collections/CollectionShare.tsx index c29a284262..11495a5b45 100644 --- a/web/apps/photos/src/components/Collections/CollectionShare.tsx +++ b/web/apps/photos/src/components/Collections/CollectionShare.tsx @@ -12,22 +12,15 @@ import Photo, { default as PhotoIcon } from "@mui/icons-material/Photo"; import PublicIcon from "@mui/icons-material/Public"; import RemoveCircleOutlineIcon from "@mui/icons-material/RemoveCircleOutline"; import WorkspacesIcon from "@mui/icons-material/Workspaces"; -import { - Dialog, - DialogProps, - FormHelperText, - Stack, - styled, - Typography, -} from "@mui/material"; +import { Dialog, Stack, styled, Typography } from "@mui/material"; import NumberAvatar from "@mui/material/Avatar"; import TextField from "@mui/material/TextField"; import Avatar from "components/pages/gallery/Avatar"; -import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; import { LoadingButton } from "ente-base/components/mui/LoadingButton"; import { - NestedSidebarDrawer, SidebarDrawer, + SidebarDrawerTitlebar, + TitledNestedSidebarDrawer, } from "ente-base/components/mui/SidebarDrawer"; import { RowButton, @@ -38,11 +31,18 @@ import { RowLabel, RowSwitch, } from "ente-base/components/RowButton"; -import { Titlebar } from "ente-base/components/Titlebar"; +import { + SingleInputForm, + type SingleInputFormProps, +} from "ente-base/components/SingleInputForm"; import { useClipboardCopy } from "ente-base/components/utils/hooks"; -import { useModalVisibility } from "ente-base/components/utils/modal"; +import { + useModalVisibility, + type ModalVisibilityProps, +} from "ente-base/components/utils/modal"; import { useBaseContext } from "ente-base/context"; import { sharedCryptoWorker } from "ente-base/crypto"; +import { isHTTP4xxError } from "ente-base/http"; import { formattedDateTime } from "ente-base/i18n-date"; import log from "ente-base/log"; import { appendCollectionKeyToShareURL } from "ente-gallery/services/share"; @@ -56,11 +56,8 @@ import { PublicLinkCreated } from "ente-new/photos/components/share/PublicLinkCr import { avatarTextColor } from "ente-new/photos/services/avatar"; import type { CollectionSummary } from "ente-new/photos/services/collection/ui"; import { usePhotosAppContext } from "ente-new/photos/types/context"; -import { FlexWrapper } from "ente-shared/components/Container"; -import SingleInputForm, { - type SingleInputFormProps, -} from "ente-shared/components/SingleInputForm"; import { CustomError, parseSharingErrorCodes } from "ente-shared/error"; +import { wait } from "ente-utils/promise"; import { Formik, type FormikHelpers } from "formik"; import { t } from "i18next"; import { GalleryContext } from "pages/gallery"; @@ -73,69 +70,52 @@ import { unshareCollection, updateShareableURL, } from "services/collectionService"; -import { getDeviceLimitOptions } from "utils/collection"; import * as Yup from "yup"; -interface CollectionShareProps { - open: boolean; - onClose: () => void; +type CollectionShareProps = ModalVisibilityProps & { collection: Collection; collectionSummary: CollectionSummary; -} +}; export const CollectionShare: React.FC = ({ + open, + onClose, + collection, collectionSummary, - ...props }) => { - const handleRootClose = () => { - props.onClose(); - }; - const handleDrawerClose: DialogProps["onClose"] = (_, reason) => { - if (reason == "backdropClick") { - handleRootClose(); - } else { - props.onClose(); - } - }; - if (!props.collection || !collectionSummary) { + if (!collection || !collectionSummary) { return <>; } + const { type } = collectionSummary; return ( - + - {type == "incomingShareCollaborator" || type == "incomingShareViewer" ? ( - + ) : ( <> )} @@ -208,16 +188,15 @@ function SharingDetails({ collection, type }) { {viewers.map((item, index) => ( - <> + } /> {index !== viewers.length - 1 && ( )} - + ))} @@ -494,6 +473,8 @@ const AddParticipant: React.FC = ({ onRootClose(); }; + const title = type == "VIEWER" ? t("add_viewers") : t("add_collaborators"); + const collectionShare: AddParticipantFormProps["callback"] = async ({ email, emails, @@ -523,6 +504,9 @@ const AddParticipant: React.FC = ({ await shareCollection(collection, email, type); await syncWithRemote(false, true); } catch (e) { + if (isHTTP4xxError(e)) { + throw new Error(t("sharing_user_does_not_exist")); + } const errorMessage = handleSharingErrors(e); throw new Error(errorMessage); } @@ -530,38 +514,20 @@ const AddParticipant: React.FC = ({ }; return ( - - - - - - + + ); }; @@ -572,27 +538,12 @@ interface AddParticipantFormValues { interface AddParticipantFormProps { callback: (props: { email?: string; emails?: string[] }) => Promise; - fieldType: "text" | "email" | "password"; - placeholder: string; buttonText: string; - submitButtonProps?: any; - initialValue?: string; - secondaryButtonAction?: () => void; - disableAutoFocus?: boolean; - hiddenPreInput?: any; - caption?: any; - hiddenPostInput?: any; - autoComplete?: string; - blockButton?: boolean; - hiddenLabel?: boolean; onClose?: () => void; optionsList?: string[]; } const AddParticipantForm: React.FC = (props) => { - const { submitButtonProps } = props; - const { sx: buttonSx, ...restSubmitButtonProps } = submitButtonProps ?? {}; - const [loading, SetLoading] = useState(false); const submitForm = async ( @@ -616,17 +567,10 @@ const AddParticipantForm: React.FC = (props) => { }; const validationSchema = useMemo(() => { - switch (props.fieldType) { - case "text": - return Yup.object().shape({ - inputValue: Yup.string().required(t("required")), - }); - case "email": - return Yup.object().shape({ - inputValue: Yup.string().email(t("invalid_email_error")), - }); - } - }, [props.fieldType]); + return Yup.object().shape({ + inputValue: Yup.string().email(t("invalid_email_error")), + }); + }, []); const handleInputFieldClick = (setFieldValue) => { setFieldValue("selectedOptions", []); @@ -634,10 +578,7 @@ const AddParticipantForm: React.FC = (props) => { return ( - initialValues={{ - inputValue: props.initialValue ?? "", - selectedOptions: [], - }} + initialValues={{ inputValue: "", selectedOptions: [] }} onSubmit={submitForm} validationSchema={validationSchema} validateOnChange={false} @@ -652,31 +593,25 @@ const AddParticipantForm: React.FC = (props) => { }) => (
- {props.hiddenPreInput} {t("add_new_email")} handleInputFieldClick(setFieldValue) } - name={props.fieldType} - {...(props.hiddenLabel - ? { placeholder: props.placeholder } - : { label: props.placeholder })} error={Boolean(errors.inputValue)} helperText={errors.inputValue} value={values.inputValue} - disabled={loading} - autoFocus={!props.disableAutoFocus} - autoComplete={props.autoComplete} /> @@ -736,57 +671,18 @@ const AddParticipantForm: React.FC = (props) => { )} - - - {props.caption} - - {props.hiddenPostInput} - - - {props.secondaryButtonAction && ( - - {t("cancel")} - - )} - - - {props.buttonText} - - - + + + {props.buttonText} + +
)} @@ -811,11 +707,12 @@ const ManageEmailShare: React.FC = ({ const { showLoadingBar, hideLoadingBar } = usePhotosAppContext(); const galleryContext = useContext(GalleryContext); - const [addParticipantView, setAddParticipantView] = useState(false); - const [manageParticipantView, setManageParticipantView] = useState(false); - - const closeAddParticipant = () => setAddParticipantView(false); - const openAddParticipant = () => setAddParticipantView(true); + const { show: showAddParticipant, props: addParticipantVisibilityProps } = + useModalVisibility(); + const { + show: showManageParticipant, + props: manageParticipantVisibilityProps, + } = useModalVisibility(); const participantType = useRef<"COLLABORATOR" | "VIEWER">(null); @@ -823,12 +720,12 @@ const ManageEmailShare: React.FC = ({ const openAddCollab = () => { participantType.current = "COLLABORATOR"; - openAddParticipant(); + showAddParticipant(); }; const openAddViewer = () => { participantType.current = "VIEWER"; - openAddParticipant(); + showAddParticipant(); }; const handleRootClose = () => { @@ -866,121 +763,106 @@ const ManageEmailShare: React.FC = ({ selectedParticipant.current = collection.sharees.find( (sharee) => sharee.email === email, ); - setManageParticipantView(true); - }; - const closeManageParticipant = () => { - setManageParticipantView(false); + showManageParticipant(); }; return ( <> - - - - - - } - > - {t("owner")} - - - } - label={isOwner ? t("you") : ownerEmail} - /> - - - - }> - {t("collaborators")} - - - {collaborators.map((item) => ( - - - openManageParticipant(item) - } - label={item} - startIcon={} - endIcon={} - /> - - - ))} + + + }> + {t("owner")} + + + } + label={isOwner ? t("you") : ownerEmail} + /> + + + + }> + {t("collaborators")} + + + {collaborators.map((item) => ( + + + openManageParticipant(item) + } + label={item} + startIcon={} + endIcon={} + /> + + + ))} - } - onClick={openAddCollab} - label={ - collaborators?.length - ? t("add_more") - : t("add_collaborators") - } - /> - - - - }> - {t("viewers")} - - - {viewers.map((item) => ( - - - openManageParticipant(item) - } - label={item} - startIcon={} - endIcon={} - /> - - - ))} - } - onClick={openAddViewer} - label={ - viewers?.length - ? t("add_more") - : t("add_viewers") - } - /> - - + } + onClick={openAddCollab} + label={ + collaborators?.length + ? t("add_more") + : t("add_collaborators") + } + /> + + + + }> + {t("viewers")} + + + {viewers.map((item) => ( + + + openManageParticipant(item) + } + label={item} + startIcon={} + endIcon={} + /> + + + ))} + } + onClick={openAddViewer} + label={ + viewers?.length + ? t("add_more") + : t("add_viewers") + } + /> + - - + + ); }; @@ -1084,83 +966,77 @@ const ManageParticipant: React.FC = ({ } return ( - - - + + + + {t("added_as")} + - - + + } + endIcon={ + selectedParticipant.role === "COLLABORATOR" && ( + + ) + } + /> + + + } + endIcon={ + selectedParticipant.role == "VIEWER" && ( + + ) + } + /> + + + + {t("collaborator_hint")} + + + - {t("added_as")} + {t("remove_participant")} } - endIcon={ - selectedParticipant.role === - "COLLABORATOR" && - } - /> - - - } - endIcon={ - selectedParticipant.role == "VIEWER" && ( - - ) - } + onClick={removeParticipant} + label={"Remove"} + startIcon={} /> - - - {t("collaborator_hint")} - - - - - {t("remove_participant")} - - - - } - /> - - - + ); }; @@ -1362,87 +1238,75 @@ const ManagePublicShareOptions: React.FC = ({ }; return ( - - - + - - - + + - - - - - - - - - - ) : ( - - ) - } - onClick={handleCopyLink} - label={t("copy_link")} - /> - - - } - onClick={disablePublicSharing} - label={t("remove_link")} - /> - - {sharableLinkError && ( - - {sharableLinkError} - - )} - + + + + + + + + ) : ( + + ) + } + onClick={handleCopyLink} + label={t("copy_link")} + /> + + + } + onClick={disablePublicSharing} + label={t("remove_link")} + /> + + {sharableLinkError && ( + + {sharableLinkError} + + )} - + ); }; @@ -1493,6 +1357,11 @@ const ManageLinkExpiry: React.FC = ({ updatePublicShareURLHelper, onRootClose, }) => { + const { show: showExpiryOptions, props: expiryOptionsVisibilityProps } = + useModalVisibility(); + + const options = useMemo(() => shareExpiryOptions(), []); + const updateDeviceExpiry = async (optionFn) => { return updatePublicShareURLHelper({ collectionID: collection.id, @@ -1500,33 +1369,17 @@ const ManageLinkExpiry: React.FC = ({ }); }; - const [shareExpiryOptionsModalView, setShareExpiryOptionsModalView] = - useState(false); - - const shareExpireOption = useMemo(() => shareExpiryOptions(), []); - - const closeShareExpiryOptionsModalView = () => - setShareExpiryOptionsModalView(false); - - const openShareExpiryOptionsModalView = () => - setShareExpiryOptionsModalView(true); - const changeShareExpiryValue = (value: number) => async () => { await updateDeviceExpiry(value); publicShareProp.validTill = value; - setShareExpiryOptionsModalView(false); - }; - - const handleRootClose = () => { - closeShareExpiryOptionsModalView(); - onRootClose(); + expiryOptionsVisibilityProps.onClose(); }; return ( <> } label={t("link_expiry")} color={ @@ -1543,43 +1396,34 @@ const ManageLinkExpiry: React.FC = ({ } /> - - - - - - {shareExpireOption.map((item, index) => ( - - - {index !== shareExpireOption.length - 1 && ( - - )} - - ))} - - + + + {options.map(({ label, value }, index) => ( + + + {index != options.length - 1 && ( + + )} + + ))} + - + ); }; -export const shareExpiryOptions = () => [ +const shareExpiryOptions = () => [ { label: t("never"), value: () => 0 }, { label: t("after_time.hour"), value: () => microsecsAfter("hour") }, { label: t("after_time.day"), value: () => microsecsAfter("day") }, @@ -1623,29 +1467,21 @@ const ManageDeviceLimit: React.FC = ({ updatePublicShareURLHelper, onRootClose, }) => { + const { show: showDeviceOptions, props: deviceOptionsVisibilityProps } = + useModalVisibility(); + + const options = useMemo(() => deviceLimitOptions(), []); + const updateDeviceLimit = async (newLimit: number) => { return updatePublicShareURLHelper({ collectionID: collection.id, deviceLimit: newLimit, }); }; - const [isChangeDeviceLimitVisible, setIsChangeDeviceLimitVisible] = - useState(false); - const deviceLimitOptions = useMemo(() => getDeviceLimitOptions(), []); - - const closeDeviceLimitChangeModal = () => - setIsChangeDeviceLimitVisible(false); - const openDeviceLimitChangeModalView = () => - setIsChangeDeviceLimitVisible(true); const changeDeviceLimitValue = (value: number) => async () => { await updateDeviceLimit(value); - setIsChangeDeviceLimitVisible(false); - }; - - const handleRootClose = () => { - closeDeviceLimitChangeModal(); - onRootClose(); + deviceOptionsVisibilityProps.onClose(); }; return ( @@ -1657,46 +1493,42 @@ const ManageDeviceLimit: React.FC = ({ ? t("none") : publicShareProp.deviceLimit.toString() } - onClick={openDeviceLimitChangeModalView} + onClick={showDeviceOptions} endIcon={} /> - - - - - - {deviceLimitOptions.map((item, index) => ( - - - {index !== - deviceLimitOptions.length - 1 && ( - - )} - - ))} - - + + + {options.map(({ label, value }, index) => ( + + + {index != options.length - 1 && ( + + )} + + ))} + - + ); }; +const deviceLimitOptions = () => + [0, 2, 5, 10, 25, 50].map((i) => ({ + label: i == 0 ? t("none") : i.toString(), + value: i, + })); + interface ManageDownloadAccessProps { publicShareProp: PublicURL; collection: Collection; @@ -1757,15 +1589,14 @@ const ManageLinkPassword: React.FC = ({ updatePublicShareURLHelper, }) => { const { showMiniDialog } = useBaseContext(); - const [changePasswordView, setChangePasswordView] = useState(false); - - const closeConfigurePassword = () => setChangePasswordView(false); + const { show: showSetPassword, props: setPasswordVisibilityProps } = + useModalVisibility(); const handlePasswordChangeSetting = async () => { if (publicShareProp.passwordEnabled) { await confirmDisablePublicUrlPassword(); } else { - setChangePasswordView(true); + showSetPassword(); } }; @@ -1792,37 +1623,39 @@ const ManageLinkPassword: React.FC = ({ checked={!!publicShareProp?.passwordEnabled} onClick={handlePasswordChangeSetting} /> - ); }; -function PublicLinkSetPassword({ +type SetPublicLinkPasswordProps = ModalVisibilityProps & + ManageLinkPasswordProps; + +const SetPublicLinkPassword: React.FC = ({ open, onClose, collection, publicShareProp, updatePublicShareURLHelper, - setChangePasswordView, -}) { - const savePassword: SingleInputFormProps["callback"] = async ( +}) => { + const savePassword: SingleInputFormProps["onSubmit"] = async ( passphrase, - setFieldError, ) => { - if (passphrase && passphrase.trim().length >= 1) { - await enablePublicUrlPassword(passphrase); - setChangePasswordView(false); - publicShareProp.passwordEnabled = true; - } else { - setFieldError("can not be empty"); - } + await enablePublicUrlPassword(passphrase); + publicShareProp.passwordEnabled = true; + onClose(); + // The onClose above will close the dialog, but if we return immediately + // from this function, then the dialog will be temporarily rendered + // without the activity indicator on the button (before the entire + // dialog disappears). This gives a ungainly visual flash, so add a wait + // long enough so that the form's activity indicator persists longer + // than it'll take for the dialog to get closed. + return wait(1000 /* 1 second */); }; const enablePublicUrlPassword = async (password: string) => { @@ -1838,10 +1671,10 @@ function PublicLinkSetPassword({ memLimit: kek.memLimit, }); }; + return (
); -} +}; diff --git a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx index 232d4ab137..37fc46b1ff 100644 --- a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx +++ b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx @@ -1,5 +1,4 @@ import { AllAlbums } from "components/Collections/AllAlbums"; -import { SetCollectionNamerAttributes } from "components/Collections/CollectionNamer"; import { CollectionShare } from "components/Collections/CollectionShare"; import { TimeStampListItem } from "components/FileList"; import { useModalVisibility } from "ente-base/components/utils/modal"; @@ -48,7 +47,6 @@ type CollectionsProps = Omit< activeCollection: Collection; setActiveCollectionID: (collectionID: number) => void; hiddenCollectionSummaries: CollectionSummaries; - setCollectionNamerAttributes: SetCollectionNamerAttributes; setPhotoListHeader: (value: TimeStampListItem) => void; filesDownloadProgressAttributesList: FilesDownloadProgressAttributes[]; setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator; @@ -84,7 +82,6 @@ export const GalleryBarAndListHeader: React.FC = ({ people, activePerson, onSelectPerson, - setCollectionNamerAttributes, setPhotoListHeader, filesDownloadProgressAttributesList, setFilesDownloadProgressAttributesCreator, @@ -145,7 +142,6 @@ export const GalleryBarAndListHeader: React.FC = ({ {...{ activeCollection, setActiveCollectionID, - setCollectionNamerAttributes, setFilesDownloadProgressAttributesCreator, isActiveCollectionDownloadInProgress, }} diff --git a/web/apps/photos/src/components/Export.tsx b/web/apps/photos/src/components/Export.tsx deleted file mode 100644 index 97736bac47..0000000000 --- a/web/apps/photos/src/components/Export.tsx +++ /dev/null @@ -1,339 +0,0 @@ -import FolderIcon from "@mui/icons-material/Folder"; -import { - Box, - Button, - Dialog, - DialogContent, - DialogTitle, - Divider, - Stack, - Tooltip, - Typography, -} from "@mui/material"; -import { isDesktop } from "ente-base/app"; -import { EnteSwitch } from "ente-base/components/EnteSwitch"; -import { LinkButton } from "ente-base/components/LinkButton"; -import { - OverflowMenu, - OverflowMenuOption, -} from "ente-base/components/OverflowMenu"; -import { EllipsizedTypography } from "ente-base/components/Typography"; -import type { ButtonishProps } from "ente-base/components/mui"; -import { DialogCloseIconButton } from "ente-base/components/mui/DialogCloseIconButton"; -import type { ModalVisibilityProps } from "ente-base/components/utils/modal"; -import { useBaseContext } from "ente-base/context"; -import { ensureElectron } from "ente-base/electron"; -import log from "ente-base/log"; -import { EnteFile } from "ente-media/file"; -import { SpaceBetweenFlex } from "ente-shared/components/Container"; -import { CustomError } from "ente-shared/error"; -import { t } from "i18next"; -import { useEffect, useState } from "react"; -import { Trans } from "react-i18next"; -import exportService, { - ExportStage, - selectAndPrepareExportDirectory, - type ExportOpts, - type ExportProgress, - type ExportSettings, -} from "services/export"; -import ExportFinished from "./ExportFinished"; -import ExportInProgress from "./ExportInProgress"; -import ExportInit from "./ExportInit"; - -type ExportProps = ModalVisibilityProps & { - allCollectionsNameByID: Map; -}; - -export const Export: React.FC = ({ - open, - onClose, - allCollectionsNameByID, -}) => { - const { showMiniDialog } = useBaseContext(); - const [exportStage, setExportStage] = useState( - ExportStage.init, - ); - const [exportFolder, setExportFolder] = useState(""); - const [continuousExport, setContinuousExport] = useState(false); - const [exportProgress, setExportProgress] = useState({ - success: 0, - failed: 0, - total: 0, - }); - const [pendingExports, setPendingExports] = useState([]); - const [lastExportTime, setLastExportTime] = useState(0); - - // ==================== - // SIDE EFFECTS - // ==================== - useEffect(() => { - if (!isDesktop) { - return; - } - try { - exportService.setUIUpdaters({ - setExportStage, - setExportProgress, - setLastExportTime, - setPendingExports, - }); - const exportSettings: ExportSettings = - exportService.getExportSettings(); - setExportFolder(exportSettings?.folder ?? null); - setContinuousExport(exportSettings?.continuousExport ?? false); - void syncExportRecord(exportSettings?.folder); - } catch (e) { - log.error("export on mount useEffect failed", e); - } - }, []); - - useEffect(() => { - if (!open) { - return; - } - void syncExportRecord(exportFolder); - }, [open]); - - // ====================== - // HELPER FUNCTIONS - // ======================= - - const verifyExportFolderExists = async () => { - if (!(await exportService.exportFolderExists(exportFolder))) { - showMiniDialog({ - title: t("export_directory_does_not_exist"), - message: ( - - ), - cancel: t("ok"), - }); - return false; - } - return true; - }; - - const syncExportRecord = async (exportFolder: string): Promise => { - try { - if (!(await exportService.exportFolderExists(exportFolder))) { - const pendingExports = - await exportService.getPendingExports(null); - setPendingExports(pendingExports); - } - const exportRecord = - await exportService.getExportRecord(exportFolder); - setExportStage(exportRecord.stage); - setLastExportTime(exportRecord.lastAttemptTimestamp); - const pendingExports = - await exportService.getPendingExports(exportRecord); - setPendingExports(pendingExports); - } catch (e) { - if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { - log.error("syncExportRecord failed", e); - } - } - }; - - // ============= - // UI functions - // ============= - - const handleChangeExportDirectoryClick = async () => { - const newFolder = await selectAndPrepareExportDirectory(); - if (!newFolder) return; - - log.info(`Export folder changed to ${newFolder}`); - exportService.updateExportSettings({ folder: newFolder }); - setExportFolder(newFolder); - await syncExportRecord(newFolder); - }; - - const toggleContinuousExport = async () => { - if (!(await verifyExportFolderExists())) return; - - const newContinuousExport = !continuousExport; - if (newContinuousExport) { - exportService.enableContinuousExport(); - } else { - exportService.disableContinuousExport(); - } - exportService.updateExportSettings({ - continuousExport: newContinuousExport, - }); - setContinuousExport(newContinuousExport); - }; - - const startExport = async (opts?: ExportOpts) => { - if (!(await verifyExportFolderExists())) return; - - await exportService.scheduleExport(opts ?? {}); - }; - - const stopExport = () => { - void exportService.stopRunningExport(); - }; - - return ( - - - {t("export_data")} - - - - - - - - - - - - - ); -}; - -function ExportDirectory({ exportFolder, changeExportDirectory, exportStage }) { - return ( - - - {t("destination")} - - {exportFolder ? ( - <> - - {exportStage === ExportStage.finished || - exportStage === ExportStage.init ? ( - - ) : ( - // Prevent layout shift. - - )} - - ) : ( - - )} - - ); -} - -const DirectoryPath = ({ path }) => ( - void ensureElectron().openDirectory(path)}> - - - {path} - - - -); - -const ChangeDirectoryOption: React.FC = ({ onClick }) => ( - - }> - {t("change_folder")} - - -); - -function ContinuousExport({ continuousExport, toggleContinuousExport }) { - return ( - - - {t("sync_continuously")} - - - - - - ); -} - -const ExportDynamicContent = ({ - exportStage, - startExport, - stopExport, - onHide, - lastExportTime, - exportProgress, - pendingExports, - allCollectionsNameByID, -}: { - exportStage: ExportStage; - startExport: (opts?: ExportOpts) => void; - stopExport: () => void; - onHide: () => void; - lastExportTime: number; - exportProgress: ExportProgress; - pendingExports: EnteFile[]; - allCollectionsNameByID: Map; -}) => { - switch (exportStage) { - case ExportStage.init: - return ; - - case ExportStage.migration: - case ExportStage.starting: - case ExportStage.exportingFiles: - case ExportStage.renamingCollectionFolders: - case ExportStage.trashingDeletedFiles: - case ExportStage.trashingDeletedCollections: - return ( - - ); - case ExportStage.finished: - return ( - startExport({ resync: true })} - /> - ); - - default: - return <>; - } -}; diff --git a/web/apps/photos/src/components/ExportFinished.tsx b/web/apps/photos/src/components/ExportFinished.tsx deleted file mode 100644 index 5a87e612ac..0000000000 --- a/web/apps/photos/src/components/ExportFinished.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { DialogActions, DialogContent, Stack, Typography } from "@mui/material"; -import { LinkButton } from "ente-base/components/LinkButton"; -import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; -import { formattedNumber } from "ente-base/i18n"; -import { formattedDateTime } from "ente-base/i18n-date"; -import { EnteFile } from "ente-media/file"; -import { SpaceBetweenFlex } from "ente-shared/components/Container"; -import { t } from "i18next"; -import { useState } from "react"; -import ExportPendingList from "./ExportPendingList"; - -interface Props { - pendingExports: EnteFile[]; - allCollectionsNameByID: Map; - onHide: () => void; - lastExportTime: number; - /** Called when the user presses the "Resync" button. */ - onResync: () => void; -} - -export default function ExportFinished(props: Props) { - const { lastExportTime } = props; - - const [pendingFileListView, setPendingFileListView] = - useState(false); - - const openPendingFileList = () => { - setPendingFileListView(true); - }; - - const closePendingFileList = () => { - setPendingFileListView(false); - }; - return ( - <> - - - - - {t("pending_items")} - - {props.pendingExports.length ? ( - - {formattedNumber(props.pendingExports.length)} - - ) : ( - - {formattedNumber(props.pendingExports.length)} - - )} - - - - {t("last_export_time")} - - - {lastExportTime - ? formattedDateTime(new Date(lastExportTime)) - : t("never")} - - - - - - - {t("close")} - - - {t("export_again")} - - - - - ); -} diff --git a/web/apps/photos/src/components/ExportInProgress.tsx b/web/apps/photos/src/components/ExportInProgress.tsx deleted file mode 100644 index 3f2566f38b..0000000000 --- a/web/apps/photos/src/components/ExportInProgress.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { - DialogActions, - DialogContent, - LinearProgress, - Typography, -} from "@mui/material"; -import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; -import { - FlexWrapper, - VerticallyCentered, -} from "ente-shared/components/Container"; -import { t } from "i18next"; -import { Trans } from "react-i18next"; -import { ExportStage, type ExportProgress } from "services/export"; - -interface Props { - exportStage: ExportStage; - exportProgress: ExportProgress; - stopExport: () => void; - closeExportDialog: () => void; -} - -export default function ExportInProgress(props: Props) { - const showIndeterminateProgress = () => { - return ( - props.exportStage === ExportStage.starting || - props.exportStage === ExportStage.migration || - props.exportStage === ExportStage.renamingCollectionFolders || - props.exportStage === ExportStage.trashingDeletedFiles || - props.exportStage === ExportStage.trashingDeletedCollections - ); - }; - return ( - <> - - - - {props.exportStage === ExportStage.starting ? ( - t("export_starting") - ) : props.exportStage === ExportStage.migration ? ( - t("export_preparing") - ) : props.exportStage === - ExportStage.renamingCollectionFolders ? ( - t("export_renaming_album_folders") - ) : props.exportStage === - ExportStage.trashingDeletedFiles ? ( - t("export_trashing_deleted_files") - ) : props.exportStage === - ExportStage.trashingDeletedCollections ? ( - t("export_trashing_deleted_albums") - ) : ( - - - ), - }} - values={{ progress: props.exportProgress }} - /> - - )} - - - {showIndeterminateProgress() ? ( - - ) : ( - - )} - - - - - - {t("close")} - - - {t("stop")} - - - - ); -} diff --git a/web/apps/photos/src/components/ExportInit.tsx b/web/apps/photos/src/components/ExportInit.tsx deleted file mode 100644 index 4a6d271edf..0000000000 --- a/web/apps/photos/src/components/ExportInit.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { DialogActions, DialogContent } from "@mui/material"; -import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; -import { t } from "i18next"; - -interface Props { - startExport: () => void; -} -export default function ExportInit({ startExport }: Props) { - return ( - - - - {t("start")} - - - - ); -} diff --git a/web/apps/photos/src/components/ExportPendingList.tsx b/web/apps/photos/src/components/ExportPendingList.tsx deleted file mode 100644 index da7f65b8ca..0000000000 --- a/web/apps/photos/src/components/ExportPendingList.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Box, styled } from "@mui/material"; -import ItemList from "components/ItemList"; -import { TitledMiniDialog } from "ente-base/components/MiniDialog"; -import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; -import { EnteFile } from "ente-media/file"; -import { ItemCard, PreviewItemTile } from "ente-new/photos/components/Tiles"; -import { FlexWrapper } from "ente-shared/components/Container"; -import { t } from "i18next"; - -interface Iprops { - isOpen: boolean; - onClose: () => void; - allCollectionsNameByID: Map; - pendingExports: EnteFile[]; -} - -export const ItemContainer = styled("div")` - position: relative; - top: 5px; - display: inline-block; - max-width: 394px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -`; - -const ExportPendingList = (props: Iprops) => { - const renderListItem = (file: EnteFile) => { - return ( - - - - - - {`${props.allCollectionsNameByID.get(file.collectionID)} / ${ - file.metadata.title - }`} - - - ); - }; - - const getItemTitle = (file: EnteFile) => { - return `${props.allCollectionsNameByID.get(file.collectionID)} / ${ - file.metadata.title - }`; - }; - - const generateItemKey = (file: EnteFile) => { - return `${file.collectionID}-${file.id}`; - }; - - return ( - - - - {t("close")} - - - ); -}; - -export default ExportPendingList; diff --git a/web/apps/photos/src/components/FileList.tsx b/web/apps/photos/src/components/FileList.tsx index dfba4ab60c..20de949231 100644 --- a/web/apps/photos/src/components/FileList.tsx +++ b/web/apps/photos/src/components/FileList.tsx @@ -6,9 +6,12 @@ import Avatar from "components/pages/gallery/Avatar"; import { assertionFailed } from "ente-base/assert"; import { Overlay } from "ente-base/components/containers"; import { isSameDay } from "ente-base/date"; +import { isDevBuild } from "ente-base/env"; import { formattedDateRelative } from "ente-base/i18n-date"; +import log from "ente-base/log"; import { downloadManager } from "ente-gallery/services/download"; import { EnteFile, enteFileDeletionDate } from "ente-media/file"; +import { fileDurationString } from "ente-media/file-metadata"; import { FileType } from "ente-media/file-type"; import { GAP_BTW_TILES, @@ -23,7 +26,6 @@ import { } from "ente-new/photos/components/PlaceholderThumbnails"; import { TileBottomTextOverlay } from "ente-new/photos/components/Tiles"; import { TRASH_SECTION } from "ente-new/photos/services/collection"; -import { FlexWrapper } from "ente-shared/components/Container"; import { t } from "i18next"; import memoize from "memoize-one"; import { GalleryContext } from "pages/gallery"; @@ -123,6 +125,16 @@ export interface FileListProps { * Not set in the context of the shared albums app. */ favoriteFileIDs?: Set; + /** + * An optional {@link TimeStampListItem} shown before all the items in the + * list. It is not sticky, and scrolls along with the content of the list. + */ + header?: TimeStampListItem; + /** + * An optional {@link TimeStampListItem} shown after all the items in the + * list. It is not sticky, and scrolls along with the content of the list. + */ + footer?: TimeStampListItem; /** * Called when the user activates the thumbnail at the given {@link index}. * @@ -148,6 +160,8 @@ export const FileList: React.FC = ({ activeCollectionID, activePersonID, favoriteFileIDs, + header, + footer, onItemClick, }) => { const galleryContext = useContext(GalleryContext); @@ -195,7 +209,9 @@ export const FileList: React.FC = ({ refreshInProgress.current = true; let timeStampList: TimeStampListItem[] = []; - if (galleryContext.photoListHeader) { + if (header) { + timeStampList.push(asFullSpanListItem(header)); + } else if (galleryContext.photoListHeader) { timeStampList.push( getPhotoListHeader(galleryContext.photoListHeader), ); @@ -219,7 +235,9 @@ export const FileList: React.FC = ({ timeStampList.push(getEmptyListItem()); } timeStampList.push(getVacuumItem(timeStampList)); - if (publicCollectionGalleryContext.credentials) { + if (footer) { + timeStampList.push(asFullSpanListItem(footer)); + } else if (publicCollectionGalleryContext.credentials) { if (publicCollectionGalleryContext.photoListFooter) { timeStampList.push( getPhotoListFooter( @@ -256,7 +274,11 @@ export const FileList: React.FC = ({ if (hasHeader) { return timeStampList; } - if (galleryContext.photoListHeader) { + // TODO(RE): Remove after audit. + if (isDevBuild) throw new Error("Unexpected header change"); + if (header) { + return [asFullSpanListItem(header), ...timeStampList]; + } else if (galleryContext.photoListHeader) { return [ getPhotoListHeader(galleryContext.photoListHeader), ...timeStampList, @@ -284,10 +306,27 @@ export const FileList: React.FC = ({ timeStampList.length > 0 && timeStampList[timeStampList.length - 1]?.tag == "publicAlbumsFooter"; + if (hasFooter) { return timeStampList; } - if (publicCollectionGalleryContext.credentials) { + // TODO(RE): Remove after audit. + if ( + isDevBuild && + (footer || + publicCollectionGalleryContext.credentials || + showAppDownloadBanner) + ) { + console.log({ timeStampList, footer, showAppDownloadBanner }); + throw new Error("Unexpected footer change"); + } + if (footer) { + return [ + ...timeStampList, + asFullSpanListItem(footer), + getAlbumsFooter(), + ]; + } else if (publicCollectionGalleryContext.credentials) { if (publicCollectionGalleryContext.photoListFooter) { return [ ...timeStampList, @@ -979,10 +1018,23 @@ const ListContainer = styled(Box, { } `; -const ListItemContainer = styled(FlexWrapper)<{ span: number }>` +const ListItemContainer = styled("div")<{ span: number }>` grid-column: span ${(props) => props.span}; + display: flex; + align-items: center; `; +const FullSpanListItemContainer = styled("div")` + grid-column: 1 / -1; + display: flex; + align-items: center; +`; + +const asFullSpanListItem = ({ item, ...rest }: TimeStampListItem) => ({ + ...rest, + item: {item}, +}); + const DateContainer = styled(ListItemContainer)( ({ theme }) => ` white-space: nowrap; @@ -1141,7 +1193,10 @@ const FileThumbnail: React.FC = ({ void downloadManager .renderableThumbnailURL(file, showPlaceholder) - .then((url) => !didCancel && setImageURL(url)); + .then((url) => !didCancel && setImageURL(url)) + .catch((e: unknown) => { + log.warn("Failed to fetch thumbnail", e); + }); return () => { didCancel = true; @@ -1198,15 +1253,13 @@ const FileThumbnail: React.FC = ({ ) : ( )} - {file.metadata.fileType === FileType.livePhoto ? ( + {file.metadata.fileType == FileType.livePhoto ? ( - + ) : ( - file.metadata.fileType === FileType.video && ( - - - + file.metadata.fileType == FileType.video && ( + ) )} {selected && } @@ -1400,3 +1453,19 @@ const SelectedOverlay = styled(Overlay)( border-radius: 4px; `, ); + +interface VideoDurationOverlayProps { + duration: string | undefined; +} + +const VideoDurationOverlay: React.FC = ({ + duration, +}) => ( + + {duration ? ( + {duration} + ) : ( + + )} + +); diff --git a/web/apps/photos/src/components/GalleryEmptyState.tsx b/web/apps/photos/src/components/GalleryEmptyState.tsx deleted file mode 100644 index f4c7bad76b..0000000000 --- a/web/apps/photos/src/components/GalleryEmptyState.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternateOutlined"; -import FolderIcon from "@mui/icons-material/FolderOutlined"; -import { Button, Stack, Typography, styled } from "@mui/material"; -import { EnteLogo } from "ente-base/components/EnteLogo"; -import { - FlexWrapper, - VerticallyCentered, -} from "ente-shared/components/Container"; -import { t } from "i18next"; -import { Trans } from "react-i18next"; -import { uploadManager } from "services/upload-manager"; - -export default function GalleryEmptyState({ openUploader }) { - return ( - - - - - }} - /> - - - {t("welcome_to_ente_subtitle")} - - - - - - - - - - ); -} - -const Wrapper = styled("div")` - display: flex; - flex-direction: column; - align-items: center; - text-align: center; -`; - -/** - * Prevent the image from being selected _and_ dragged, since dragging it - * triggers the our dropdown selector overlay. - */ -const NonDraggableImage = styled("img")` - pointer-events: none; - user-select: none; -`; diff --git a/web/apps/photos/src/components/ItemList.tsx b/web/apps/photos/src/components/ItemList.tsx deleted file mode 100644 index f9648460ea..0000000000 --- a/web/apps/photos/src/components/ItemList.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { Box, Tooltip } from "@mui/material"; -import memoize from "memoize-one"; -import React, { ReactElement } from "react"; -import { - FixedSizeList as List, - ListChildComponentProps, - ListItemKeySelector, - areEqual, -} from "react-window"; - -export interface ItemListProps { - items: T[]; - generateItemKey: (item: T) => string | number; - getItemTitle: (item: T) => string; - renderListItem: (item: T) => React.JSX.Element; - maxHeight?: number; - itemSize?: number; -} - -interface ItemData { - renderListItem: (item: T) => React.JSX.Element; - getItemTitle: (item: T) => string; - items: T[]; -} - -const createItemData: ( - renderListItem: (item: T) => React.JSX.Element, - getItemTitle: (item: T) => string, - items: T[], -) => ItemData = memoize((renderListItem, getItemTitle, items) => ({ - renderListItem, - getItemTitle, - items, -})); - -// @ts-expect-error "TODO(MR): Understand and fix the type error here" -const Row: ({ - index, - style, - data, -}: ListChildComponentProps>) => ReactElement = React.memo( - ({ index, style, data }) => { - const { renderListItem, items, getItemTitle } = data; - return ( - -
{renderListItem(items[index])}
-
- ); - }, - areEqual, -); - -export default function ItemList(props: ItemListProps) { - const itemData = createItemData( - props.renderListItem, - props.getItemTitle, - props.items, - ); - - const getItemKey: ListItemKeySelector> = (index, data) => { - const { items } = data; - return props.generateItemKey(items[index]); - }; - - return ( - - - {Row} - - - ); -} diff --git a/web/apps/photos/src/components/Sidebar.tsx b/web/apps/photos/src/components/Sidebar.tsx index d79afb1942..d82cacc3c7 100644 --- a/web/apps/photos/src/components/Sidebar.tsx +++ b/web/apps/photos/src/components/Sidebar.tsx @@ -7,10 +7,10 @@ import HealthAndSafetyIcon from "@mui/icons-material/HealthAndSafety"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import LockOutlinedIcon from "@mui/icons-material/LockOutlined"; import NorthEastIcon from "@mui/icons-material/NorthEast"; +import ScienceIcon from "@mui/icons-material/Science"; import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; import { Box, - Button, Dialog, DialogContent, Divider, @@ -33,15 +33,16 @@ import { RowButtonDivider, RowButtonGroup, RowButtonGroupHint, + RowButtonGroupTitle, RowSwitch, } from "ente-base/components/RowButton"; import { SpacedRow } from "ente-base/components/containers"; import { ActivityIndicator } from "ente-base/components/mui/ActivityIndicator"; import { DialogCloseIconButton } from "ente-base/components/mui/DialogCloseIconButton"; +import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; import { - NestedSidebarDrawer, SidebarDrawer, - SidebarDrawerTitlebar, + TitledNestedSidebarDrawer, type NestedSidebarDrawerVisibilityProps, } from "ente-base/components/mui/SidebarDrawer"; import { useIsSmallWidth } from "ente-base/components/utils/hooks"; @@ -61,6 +62,10 @@ import log from "ente-base/log"; import { savedLogs } from "ente-base/log-web"; import { customAPIHost } from "ente-base/origins"; import { downloadString } from "ente-base/utils/web"; +import { + isHLSGenerationSupported, + toggleHLSGeneration, +} from "ente-gallery/services/video"; import { DeleteAccount } from "ente-new/photos/components/DeleteAccount"; import { DropdownInput } from "ente-new/photos/components/DropdownInput"; import { MLSettings } from "ente-new/photos/components/sidebar/MLSettings"; @@ -71,6 +76,7 @@ import { } from "ente-new/photos/components/utils/dialog"; import { downloadAppDialogAttributes } from "ente-new/photos/components/utils/download"; import { + useHLSGenerationStatusSnapshot, useSettingsSnapshot, useUserDetailsSnapshot, } from "ente-new/photos/components/utils/use-snapshot"; @@ -80,6 +86,7 @@ import { TRASH_SECTION, } from "ente-new/photos/services/collection"; import type { CollectionSummaries } from "ente-new/photos/services/collection/ui"; +import exportService from "ente-new/photos/services/export"; import { isMLSupported } from "ente-new/photos/services/ml"; import { isDevBuildAndUser, @@ -106,10 +113,6 @@ import { } from "ente-new/photos/services/user-details"; import { usePhotosAppContext } from "ente-new/photos/types/context"; import { initiateEmail, openURL } from "ente-new/photos/utils/web"; -import { - FlexWrapper, - VerticallyCentered, -} from "ente-shared/components/Container"; import { t } from "i18next"; import { useRouter } from "next/router"; import { GalleryContext } from "pages/gallery"; @@ -123,7 +126,6 @@ import React, { } from "react"; import { Trans } from "react-i18next"; import { getUncategorizedCollection } from "services/collectionService"; -import exportService from "services/export"; import { testUpload } from "../../tests/upload.test"; import { SubscriptionCard } from "./SubscriptionCard"; @@ -211,13 +213,10 @@ const UserDetailsSection: React.FC = ({ onShowPlanSelector, }) => { const userDetails = useUserDetailsSnapshot(); - const [memberSubscriptionManageView, setMemberSubscriptionManageView] = - useState(false); - - const openMemberSubscriptionManage = () => - setMemberSubscriptionManageView(true); - const closeMemberSubscriptionManage = () => - setMemberSubscriptionManageView(false); + const { + show: showManageMemberSubscription, + props: manageMemberSubscriptionVisibilityProps, + } = useModalVisibility(); useEffect(() => { if (sidebarOpen) void syncUserDetails(); @@ -233,7 +232,7 @@ const UserDetailsSection: React.FC = ({ const handleSubscriptionCardClick = () => { if (isNonAdminFamilyMember) { - openMemberSubscriptionManage(); + showManageMemberSubscription(); } else { if ( userDetails && @@ -268,11 +267,10 @@ const UserDetailsSection: React.FC = ({ /> )} - {isNonAdminFamilyMember && ( - )} @@ -372,7 +370,15 @@ const SubscriptionStatus: React.FC = ({ ); }; -function MemberSubscriptionManage({ open, userDetails, onClose }) { +type ManageMemberSubscriptionProps = ModalVisibilityProps & { + userDetails: UserDetails; +}; + +const ManageMemberSubscription: React.FC = ({ + open, + onClose, + userDetails, +}) => { const { showMiniDialog } = useBaseContext(); const fullScreen = useIsSmallWidth(); @@ -387,10 +393,6 @@ function MemberSubscriptionManage({ open, userDetails, onClose }) { }, }); - if (!userDetails) { - return <>; - } - return ( @@ -403,7 +405,7 @@ function MemberSubscriptionManage({ open, userDetails, onClose }) { - + {t("subscription_info_family")} @@ -412,27 +414,24 @@ function MemberSubscriptionManage({ open, userDetails, onClose }) { {familyAdminEmail(userDetails) ?? ""} - - - - - + + {t("leave_family_plan")} + + ); -} +}; type ShortcutSectionProps = SectionProps & { collectionSummaries: SidebarProps["collectionSummaries"]; @@ -658,6 +657,7 @@ const InfoSection: React.FC = () => { type AccountProps = NestedSidebarDrawerVisibilityProps & Pick; + const Account: React.FC = ({ open, onClose, @@ -689,55 +689,49 @@ const Account: React.FC = ({ }; return ( - - - - - - - } - label={t("recovery_key")} - onClick={showRecoveryKey} - /> - - - - - - - - - - - - - - - + + + + + } + label={t("recovery_key")} + onClick={showRecoveryKey} + /> + + + + + + + + + + + + + + = ({ {...deleteAccountVisibilityProps} {...{ onAuthenticateUser }} /> - + ); }; @@ -769,6 +763,9 @@ const Preferences: React.FC = ({ const { show: showMLSettings, props: mlSettingsVisibilityProps } = useModalVisibility(); + const hlsGenStatusSnapshot = useHLSGenerationStatusSnapshot(); + const isHLSGenerationEnabled = !!hlsGenStatusSnapshot?.enabled; + useEffect(() => { if (open) void syncSettings(); }, [open]); @@ -779,37 +776,48 @@ const Preferences: React.FC = ({ }; return ( - - - + + + + + {isMLSupported && ( + + } + label={t("ml_search")} + onClick={showMLSettings} + /> + + )} + } + label={t("map")} + onClick={showMapSettings} /> - - - - - {isMLSupported && ( + } + label={t("advanced")} + onClick={showAdvancedSettings} + /> + {isHLSGenerationSupported && ( + + }> + {t("labs")} + - } - label={t("ml_search")} - onClick={showMLSettings} + void toggleHLSGeneration()} /> - )} - } - label={t("map")} - onClick={showMapSettings} - /> - } - label={t("advanced")} - onClick={showAdvancedSettings} - /> - + + )} = ({ {...mlSettingsVisibilityProps} onRootClose={handleRootClose} /> - + ); }; @@ -957,28 +965,21 @@ const MapSettings: React.FC = ({ }; return ( - - - - - - - - - + + + + - + ); }; @@ -1015,41 +1016,35 @@ const AdvancedSettings: React.FC = ({ void electron?.toggleAutoLaunch().then(refreshAutoLaunchEnabled); return ( - - - - - - - - - - {t("faster_upload_description")} - - - {electron && ( - - - - )} + + + + + + + {t("faster_upload_description")} + + {electron && ( + + + + )} - + ); }; @@ -1089,71 +1084,68 @@ const Help: React.FC = ({ }; return ( - - - + + + } + label={t("ente_help")} + onClick={handleHelp} + /> + + + } + label={t("blog")} + onClick={handleBlog} + /> + + } + label={t("request_feature")} + onClick={handleRequestFeature} + /> + + + } + label={ + + + {t("support")} + + + } + onClick={handleSupport} + /> + + + + + {t("view_logs")} + + } + onClick={confirmViewLogs} /> - - - } - label={t("ente_help")} - onClick={handleHelp} - /> - - - } - label={t("blog")} - onClick={handleBlog} - /> - - } - label={t("request_feature")} - onClick={handleRequestFeature} - /> - - - } - label={ - - - {t("support")} - - - } - onClick={handleSupport} - /> - - - + {isDevBuildAndUser() && ( - {t("view_logs")} + {ut("Test upload")} } - onClick={confirmViewLogs} + onClick={testUpload} /> - {isDevBuildAndUser() && ( - - {ut("Test upload")} - - } - onClick={testUpload} - /> - )} - + )} - + ); }; diff --git a/web/apps/photos/src/components/Upload.tsx b/web/apps/photos/src/components/Upload.tsx index 7738324f4f..ff85b567ff 100644 --- a/web/apps/photos/src/components/Upload.tsx +++ b/web/apps/photos/src/components/Upload.tsx @@ -18,6 +18,7 @@ import { SpacedRow } from "ente-base/components/containers"; import { DialogCloseIconButton } from "ente-base/components/mui/DialogCloseIconButton"; import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; import { RowButton } from "ente-base/components/RowButton"; +import { SingleInputDialog } from "ente-base/components/SingleInputDialog"; import { useIsTouchscreen } from "ente-base/components/utils/hooks"; import { useModalVisibility, @@ -27,12 +28,17 @@ import { useBaseContext } from "ente-base/context"; import { basename, dirname, joinPath } from "ente-base/file-name"; import log from "ente-base/log"; import type { CollectionMapping, Electron, ZipItem } from "ente-base/types/ipc"; +import { type UploadTypeSelectorIntent } from "ente-gallery/components/Upload"; import { useFileInput } from "ente-gallery/components/utils/use-file-input"; -import type { - FileAndPath, - UploadItem, - UploadPhase, +import { + groupItemsBasedOnParentFolder, + uploadPathPrefix, + type FileAndPath, + type UploadItem, + type UploadItemAndPath, + type UploadPhase, } from "ente-gallery/services/upload"; +import type { ParsedMetadataJSON } from "ente-gallery/services/upload/metadata-json"; import type { Collection } from "ente-media/collection"; import type { EnteFile } from "ente-media/file"; import { UploaderNameInput } from "ente-new/albums/components/UploaderNameInput"; @@ -40,7 +46,6 @@ import { CollectionMappingChoice } from "ente-new/photos/components/CollectionMa import type { CollectionSelectorAttributes } from "ente-new/photos/components/CollectionSelector"; import { downloadAppDialogAttributes } from "ente-new/photos/components/utils/download"; import { getLatestCollections } from "ente-new/photos/services/collections"; -import { exportMetadataDirectoryName } from "ente-new/photos/services/export"; import { redirectToCustomerPortal } from "ente-new/photos/services/user-details"; import { usePhotosAppContext } from "ente-new/photos/types/context"; import { CustomError } from "ente-shared/error"; @@ -71,11 +76,8 @@ import watcher from "services/watch"; import { SetLoading } from "types/gallery"; import { getOrCreateAlbum } from "utils/collection"; import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; -import { SetCollectionNamerAttributes } from "./Collections/CollectionNamer"; import { UploadProgress } from "./UploadProgress"; -export type UploadTypeSelectorIntent = "upload" | "import" | "collect"; - interface UploadProps { syncWithRemote: (force?: boolean, silent?: boolean) => Promise; closeUploadTypeSelector: () => void; @@ -89,7 +91,6 @@ interface UploadProps { * Close the collection selector if it is open. */ onCloseCollectionSelector?: () => void; - setCollectionNamerAttributes?: SetCollectionNamerAttributes; setLoading: SetLoading; setShouldDisableDropzone: (value: boolean) => void; showCollectionSelector?: () => void; @@ -118,8 +119,6 @@ interface UploadProps { type UploadType = "files" | "folders" | "zips"; -type UploadItemAndPath = [UploadItem, string]; - /** * Top level component that houses the infrastructure for handling uploads. */ @@ -152,12 +151,17 @@ export const Upload: React.FC = ({ useState(new Map()); const [percentComplete, setPercentComplete] = useState(0); const [hasLivePhotos, setHasLivePhotos] = useState(false); + const [prefilledNewAlbumName, setPrefilledNewAlbumName] = useState(""); const [openCollectionMappingChoice, setOpenCollectionMappingChoice] = useState(false); const [importSuggestion, setImportSuggestion] = useState( defaultImportSuggestion, ); + const { + show: showNewAlbumNameInput, + props: newAlbumNameInputVisibilityProps, + } = useModalVisibility(); const { show: showUploaderNameInput, props: uploaderNameInputVisibilityProps, @@ -535,8 +539,10 @@ export const Upload: React.FC = ({ if (importSuggestion.hasNestedFolders) { showNextModal = () => setOpenCollectionMappingChoice(true); } else { - showNextModal = () => - showCollectionCreateModal(importSuggestion.rootFolderName); + showNextModal = () => { + setPrefilledNewAlbumName(importSuggestion.rootFolderName); + showNewAlbumNameInput(); + }; } props.onOpenCollectionSelector({ @@ -550,7 +556,7 @@ export const Upload: React.FC = ({ const preCollectionCreationAction = async () => { props.onCloseCollectionSelector?.(); - props.setShouldDisableDropzone(!uploadManager.shouldAllowNewUpload()); + props.setShouldDisableDropzone(uploadManager.isUploadInProgress()); setUploadPhase("preparing"); setUploadProgressView(true); }; @@ -561,8 +567,9 @@ export const Upload: React.FC = ({ ) => { await preCollectionCreationAction(); const uploadItemsWithCollection = uploadItemsAndPaths.current.map( - ([uploadItem], index) => ({ + ([uploadItem, path], index) => ({ uploadItem, + pathPrefix: uploadPathPrefix(path), localID: index, collectionID: collection.id, }), @@ -582,14 +589,17 @@ export const Upload: React.FC = ({ await preCollectionCreationAction(); let uploadItemsWithCollection: UploadItemWithCollection[] = []; const collections: Collection[] = []; - let collectionNameToUploadItems = new Map(); + let collectionNameToUploadItems = new Map< + string, + UploadItemAndPath[] + >(); if (mapping == "root") { collectionNameToUploadItems.set( collectionName, - uploadItemsAndPaths.current.map(([i]) => i), + uploadItemsAndPaths.current, ); } else { - collectionNameToUploadItems = groupFilesBasedOnParentFolder( + collectionNameToUploadItems = groupItemsBasedOnParentFolder( uploadItemsAndPaths.current, collectionName, ); @@ -609,8 +619,9 @@ export const Upload: React.FC = ({ props.setCollections([...existingCollections, ...collections]); uploadItemsWithCollection = [ ...uploadItemsWithCollection, - ...uploadItems.map((uploadItem) => ({ + ...uploadItems.map(([uploadItem, path]) => ({ localID: index++, + pathPrefix: uploadPathPrefix(path), collectionID: collection.id, uploadItem, })), @@ -642,8 +653,10 @@ export const Upload: React.FC = ({ await currentUploadPromise.current; }; - const preUploadAction = async () => { - uploadManager.prepareForNewUpload(); + const preUploadAction = async ( + parsedMetadataJSONMap?: Map, + ) => { + uploadManager.prepareForNewUpload(parsedMetadataJSONMap); setUploadProgressView(true); await props.syncWithRemote(true, true); }; @@ -704,10 +717,10 @@ export const Upload: React.FC = ({ const retryFailed = async () => { try { log.info("Retrying failed uploads"); - const { items, collections } = - uploadManager.getFailedItemsWithCollections(); + const { items, collections, parsedMetadataJSONMap } = + uploadManager.failedItemState(); const uploaderName = uploadManager.getUploaderName(); - await preUploadAction(); + await preUploadAction(parsedMetadataJSONMap); await uploadManager.uploadItems(items, collections, uploaderName); } catch (e) { log.error("Retrying failed uploads failed", e); @@ -754,15 +767,6 @@ export const Upload: React.FC = ({ uploadFilesToNewCollections("root", collectionName); }; - const showCollectionCreateModal = (suggestedName: string) => { - props.setCollectionNamerAttributes({ - title: t("new_album"), - buttonText: t("create"), - autoFilledName: suggestedName, - callback: uploadToSingleNewCollection, - }); - }; - const cancelUploads = () => { uploadManager.cancelRunningUpload(); }; @@ -852,6 +856,16 @@ export const Upload: React.FC = ({ finishedUploads={finishedUploads} cancelUploads={cancelUploads} /> + [j], - * b => [e,f,g], - * c => [h, i] - * ] - * - * @param defaultFolderName Optional collection name to use for any rooted files - * that do not have a parent folder. The function will throw if a default is not - * provided and we encounter any such files without a parent. - */ -const groupFilesBasedOnParentFolder = ( - uploadItemAndPaths: UploadItemAndPath[], - defaultFolderName: string | undefined, -) => { - const result = new Map(); - for (const [uploadItem, pathOrName] of uploadItemAndPaths) { - let folderPath = pathOrName.substring(0, pathOrName.lastIndexOf("/")); - // If the parent folder of a file is "metadata", then we consider it to - // be part of the parent folder. - // - // e.g. for FileList - // - // [a/x.png, a/metadata/x.png.json] - // - // they will both be grouped into the collection "a". This is so that we - // cluster the metadata json files in the same collection as the file it - // is for. - if (folderPath.endsWith(exportMetadataDirectoryName)) { - folderPath = folderPath.substring(0, folderPath.lastIndexOf("/")); - } - let folderName = folderPath.substring(folderPath.lastIndexOf("/") + 1); - if (!folderName) { - if (!defaultFolderName) - throw Error(`Leaf file (without default): ${folderPath}`); - folderName = defaultFolderName; - } - if (!result.has(folderName)) result.set(folderName, []); - result.get(folderName).push(uploadItem); - } - return result; -}; - const setPendingUploads = async ( electron: Electron, collections: Collection[], diff --git a/web/apps/photos/src/components/UploadProgress.tsx b/web/apps/photos/src/components/UploadProgress.tsx index ee81e62b27..20a862ff37 100644 --- a/web/apps/photos/src/components/UploadProgress.tsx +++ b/web/apps/photos/src/components/UploadProgress.tsx @@ -18,23 +18,34 @@ import { Snackbar, Stack, styled, + Tooltip, Typography, type AccordionProps, type DialogProps, } from "@mui/material"; -import ItemList from "components/ItemList"; +import { SpacedRow } from "ente-base/components/containers"; import { FilledIconButton } from "ente-base/components/mui"; import { useBaseContext } from "ente-base/context"; import { formattedListJoin } from "ente-base/i18n"; -import { - type UploadPhase, - type UploadResult, -} from "ente-gallery/services/upload"; -import { SpaceBetweenFlex } from "ente-shared/components/Container"; +import { type UploadPhase } from "ente-gallery/services/upload"; import { t } from "i18next"; -import React, { createContext, useContext, useEffect, useState } from "react"; +import memoize from "memoize-one"; +import React, { + createContext, + ReactElement, + useContext, + useEffect, + useState, +} from "react"; import { Trans } from "react-i18next"; +import { + areEqual, + FixedSizeList as List, + ListChildComponentProps, + ListItemKeySelector, +} from "react-window"; import type { + FinishedUploadResult, InProgressUpload, SegregatedFinishedUploads, UploadCounter, @@ -172,22 +183,20 @@ const UploadProgressTitle: React.FC = () => { return ( - + {t("file_upload")} - - - - {expanded ? : } - - - - - - - + + + {expanded ? : } + + + + + + ); }; @@ -251,7 +260,6 @@ const uploadedFileCount = ( let c = 0; c += finishedUploads.get("uploaded")?.length ?? 0; c += finishedUploads.get("uploadedWithStaticThumbnail")?.length ?? 0; - c += finishedUploads.get("addedSymlink")?.length ?? 0; return c; }; @@ -474,7 +482,7 @@ const NotUploadSectionHeader = styled("div")( ); interface ResultSectionProps { - uploadResult: UploadResult; + uploadResult: FinishedUploadResult; sectionTitle: string; sectionInfo?: React.ReactNode; } @@ -561,6 +569,93 @@ const TitleText: React.FC = ({ title, count }) => ( ); +interface ItemListProps { + items: T[]; + generateItemKey: (item: T) => string | number; + getItemTitle: (item: T) => string; + renderListItem: (item: T) => React.JSX.Element; + maxHeight?: number; + itemSize?: number; +} + +interface ItemData { + renderListItem: (item: T) => React.JSX.Element; + getItemTitle: (item: T) => string; + items: T[]; +} + +const createItemData: ( + renderListItem: (item: T) => React.JSX.Element, + getItemTitle: (item: T) => string, + items: T[], +) => ItemData = memoize((renderListItem, getItemTitle, items) => ({ + renderListItem, + getItemTitle, + items, +})); + +// @ts-expect-error "TODO(MR): Understand and fix the type error here" +const Row: ({ + index, + style, + data, +}: ListChildComponentProps>) => ReactElement = React.memo( + ({ index, style, data }) => { + const { renderListItem, items, getItemTitle } = data; + return ( + +
{renderListItem(items[index])}
+
+ ); + }, + areEqual, +); + +function ItemList(props: ItemListProps) { + const itemData = createItemData( + props.renderListItem, + props.getItemTitle, + props.items, + ); + + const getItemKey: ListItemKeySelector> = (index, data) => { + const { items } = data; + return props.generateItemKey(items[index]); + }; + + return ( + + + {Row} + + + ); +} + const DoneFooter: React.FC = () => { const { uploadPhase, finishedUploads, retryFailed, onClose } = useContext( UploadProgressContext, diff --git a/web/apps/photos/src/components/WatchFolder.tsx b/web/apps/photos/src/components/WatchFolder.tsx index 57a038bd85..c88e332776 100644 --- a/web/apps/photos/src/components/WatchFolder.tsx +++ b/web/apps/photos/src/components/WatchFolder.tsx @@ -177,18 +177,18 @@ const NoWatches: React.FC = () => ( {t("watch_folders_hint_1")} - - - + + + {t("watch_folders_hint_2")} - - - - - + + + + + {t("watch_folders_hint_3")} - - + + ); diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx index f17ce008ee..cdd5849886 100644 --- a/web/apps/photos/src/pages/_app.tsx +++ b/web/apps/photos/src/pages/_app.tsx @@ -22,6 +22,10 @@ import { BaseContext, deriveBaseContext } from "ente-base/context"; import log from "ente-base/log"; import { logStartupBanner } from "ente-base/log-web"; import { AppUpdate } from "ente-base/types/ipc"; +import { + initVideoProcessing, + isHLSGenerationSupported, +} from "ente-gallery/services/video"; import { Notification } from "ente-new/photos/components/Notification"; import { ThemedLoadingBar } from "ente-new/photos/components/ThemedLoadingBar"; import { @@ -29,6 +33,7 @@ import { updateReadyToInstallDialogAttributes, } from "ente-new/photos/components/utils/download"; import { useLoadingBar } from "ente-new/photos/components/utils/use-loading-bar"; +import { resumeExportsIfNeeded } from "ente-new/photos/services/export"; import { runMigrations } from "ente-new/photos/services/migration"; import { initML, isMLSupported } from "ente-new/photos/services/ml"; import { getFamilyPortalRedirectURL } from "ente-new/photos/services/user-details"; @@ -43,7 +48,6 @@ import { t } from "i18next"; import type { AppProps } from "next/app"; import { useRouter } from "next/router"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { resumeExportsIfNeeded } from "services/export"; import { photosLogout } from "services/logout"; import "photoswipe/dist/photoswipe.css"; @@ -108,6 +112,7 @@ const App: React.FC = ({ Component, pageProps }) => { }; if (isMLSupported) initML(); + if (isHLSGenerationSupported) void initVideoProcessing(); electron.onOpenEnteURL(handleOpenEnteURL); electron.onAppUpdateAvailable(showUpdateDialog); diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index a67cd70716..813406984b 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -3,11 +3,7 @@ import FileUploadOutlinedIcon from "@mui/icons-material/FileUploadOutlined"; import MenuIcon from "@mui/icons-material/Menu"; import { IconButton, Stack, Typography } from "@mui/material"; import { AuthenticateUser } from "components/AuthenticateUser"; -import CollectionNamer, { - CollectionNamerAttributes, -} from "components/Collections/CollectionNamer"; import { GalleryBarAndListHeader } from "components/Collections/GalleryBarAndListHeader"; -import { Export } from "components/Export"; import { TimeStampListItem } from "components/FileList"; import { FileListWithViewer } from "components/FileListWithViewer"; import { @@ -15,14 +11,15 @@ import { FilesDownloadProgressAttributes, } from "components/FilesDownloadProgress"; import { FixCreationTime } from "components/FixCreationTime"; -import GalleryEmptyState from "components/GalleryEmptyState"; import { Sidebar } from "components/Sidebar"; -import { Upload, type UploadTypeSelectorIntent } from "components/Upload"; +import { Upload } from "components/Upload"; import SelectedFileOptions from "components/pages/gallery/SelectedFileOptions"; import { sessionExpiredDialogAttributes } from "ente-accounts/components/utils/dialog"; import { stashRedirect } from "ente-accounts/services/redirect"; +import { isSessionInvalid } from "ente-accounts/services/session"; import type { MiniDialogAttributes } from "ente-base/components/MiniDialog"; import { NavbarBase } from "ente-base/components/Navbar"; +import { SingleInputDialog } from "ente-base/components/SingleInputDialog"; import { CenteredRow } from "ente-base/components/containers"; import { TranslucentLoadingOverlay } from "ente-base/components/loaders"; import type { ButtonishProps } from "ente-base/components/mui"; @@ -32,7 +29,13 @@ import { useIsSmallWidth } from "ente-base/components/utils/hooks"; import { useModalVisibility } from "ente-base/components/utils/modal"; import { useBaseContext } from "ente-base/context"; import log from "ente-base/log"; +import { + clearSessionStorage, + haveCredentialsInSession, + masterKeyFromSessionIfLoggedIn, +} from "ente-base/session"; import { FullScreenDropZone } from "ente-gallery/components/FullScreenDropZone"; +import { type UploadTypeSelectorIntent } from "ente-gallery/components/Upload"; import { type Collection } from "ente-media/collection"; import { type EnteFile } from "ente-media/file"; import { @@ -43,6 +46,7 @@ import { CollectionSelector, type CollectionSelectorAttributes, } from "ente-new/photos/components/CollectionSelector"; +import { Export } from "ente-new/photos/components/Export"; import { PlanSelector } from "ente-new/photos/components/PlanSelector"; import { SearchBar, @@ -50,12 +54,14 @@ import { } from "ente-new/photos/components/SearchBar"; import { WhatsNew } from "ente-new/photos/components/WhatsNew"; import { + GalleryEmptyState, PeopleEmptyState, SearchResultsHeader, } from "ente-new/photos/components/gallery"; import { constructUserIDToEmailMap, createShareeSuggestionEmails, + validateKey, } from "ente-new/photos/components/gallery/helpers"; import { useGalleryReducer, @@ -70,6 +76,7 @@ import { } from "ente-new/photos/services/collection"; import { areOnlySystemCollections } from "ente-new/photos/services/collection/ui"; import { getAllLocalCollections } from "ente-new/photos/services/collections"; +import exportService from "ente-new/photos/services/export"; import { getLocalFiles, getLocalTrashedFiles, @@ -92,9 +99,6 @@ import { verifyStripeSubscription, } from "ente-new/photos/services/user-details"; import { usePhotosAppContext } from "ente-new/photos/types/context"; -import { FlexWrapper } from "ente-shared/components/Container"; -import { getRecoveryKey } from "ente-shared/crypto/helpers"; -import { CustomError } from "ente-shared/error"; import { getData } from "ente-shared/storage/localStorage"; import { getToken, @@ -103,7 +107,7 @@ import { setIsFirstLogin, setJustSignedUp, } from "ente-shared/storage/localStorage/helpers"; -import { clearKeys, getKey } from "ente-shared/storage/sessionStorage"; +import { getKey } from "ente-shared/storage/sessionStorage"; import { t } from "i18next"; import { useRouter, type NextRouter } from "next/router"; import { createContext, useCallback, useEffect, useRef, useState } from "react"; @@ -115,9 +119,7 @@ import { createUnCategorizedCollection, removeFromFavorites, } from "services/collectionService"; -import exportService from "services/export"; import { uploadManager } from "services/upload-manager"; -import { isTokenValid } from "services/userService"; import { GalleryContextType, SelectedState, @@ -131,6 +133,17 @@ import { } from "utils/collection"; import { getSelectedFiles, handleFileOp, type FileOp } from "utils/file"; +/** + * Options to customize the behaviour of the sync with remote that gets + * triggered on various actions within the gallery and its descendants. + */ +interface SyncWithRemoteOpts { + /** Force a sync to happen (default: no) */ + force?: boolean; + /** Perform the sync without showing a global loading bar (default: no) */ + silent?: boolean; +} + const defaultGalleryContext: GalleryContextType = { setActiveCollectionID: () => null, syncWithRemote: () => null, @@ -178,22 +191,17 @@ const Page: React.FC = () => { context: { mode: "albums", collectionID: ALL_SECTION }, }); const [blockingLoad, setBlockingLoad] = useState(false); - const [collectionNamerAttributes, setCollectionNamerAttributes] = - useState(null); - const [collectionNamerView, setCollectionNamerView] = useState(false); const [shouldDisableDropzone, setShouldDisableDropzone] = useState(false); const [dragAndDropFiles, setDragAndDropFiles] = useState( [], ); const [isFileViewerOpen, setIsFileViewerOpen] = useState(false); - const syncInProgress = useRef(false); - const syncInterval = useRef | undefined>( - undefined, - ); - const resync = useRef<{ force: boolean; silent: boolean } | undefined>( - undefined, - ); + /**`true` if a sync is currently in progress. */ + const isSyncing = useRef(false); + /** Set to the {@link SyncWithRemoteOpts} of the last sync that was enqueued + while one was already in progress. */ + const resyncOpts = useRef(undefined); const [userIDToEmailMap, setUserIDToEmailMap] = useState>(null); @@ -222,6 +230,9 @@ const Page: React.FC = () => { filesDownloadProgressAttributesList, setFilesDownloadProgressAttributesList, ] = useState([]); + const [, setPostCreateAlbumOp] = useState( + undefined, + ); const [openCollectionSelector, setOpenCollectionSelector] = useState(false); const [collectionSelectorAttributes, setCollectionSelectorAttributes] = @@ -241,6 +252,8 @@ const Page: React.FC = () => { show: showAuthenticateUser, props: authenticateUserVisibilityProps, } = useModalVisibility(); + const { show: showAlbumNameInput, props: albumNameInputVisibilityProps } = + useModalVisibility(); const onAuthenticateCallback = useRef<(() => void) | undefined>(undefined); @@ -286,32 +299,21 @@ const Page: React.FC = () => { const router = useRouter(); - // Ensure that the keys in local storage are not malformed by verifying that - // the recoveryKey can be decrypted with the masterKey. - // Note: This is not bullet-proof. - const validateKey = async () => { - try { - await getRecoveryKey(); - return true; - } catch { - logout(); - return false; - } - }; - useEffect(() => { - const key = getKey("encryptionKey"); const token = getToken(); - if (!key || !token) { + if (!haveCredentialsInSession() || !token) { stashRedirect("/gallery"); router.push("/"); return; } preloadImage("/images/subscription-card-background"); + const electron = globalThis.electron; - const main = async () => { - const valid = await validateKey(); - if (!valid) { + let syncIntervalID: ReturnType | undefined; + + void (async () => { + if (!(await validateKey())) { + logout(); return; } initSettings(); @@ -335,21 +337,23 @@ const Page: React.FC = () => { hiddenFiles: await getLocalFiles("hidden"), trashedFiles: await getLocalTrashedFiles(), }); - await syncWithRemote(true); + await syncWithRemote({ force: true }); setIsFirstLoad(false); setJustSignedUp(false); - syncInterval.current = setInterval( - () => syncWithRemote(false, true), + syncIntervalID = setInterval( + () => syncWithRemote({ silent: true }), 5 * 60 * 1000 /* 5 minutes */, ); if (electron) { - electron.onMainWindowFocus(() => syncWithRemote(false, true)); + electron.onMainWindowFocus(() => + syncWithRemote({ silent: true }), + ); if (await shouldShowWhatsNew(electron)) showWhatsNew(); } - }; - main(); + })(); + return () => { - clearInterval(syncInterval.current); + clearInterval(syncIntervalID); if (electron) electron.onMainWindowFocus(undefined); }; }, []); @@ -371,10 +375,6 @@ const Page: React.FC = () => { ); }, [user, normalCollections, familyData]); - useEffect(() => { - collectionNamerAttributes && setCollectionNamerView(true); - }, [collectionNamerAttributes]); - useEffect(() => { if (typeof activeCollectionID == "undefined" || !router.isReady) { return; @@ -451,12 +451,12 @@ const Page: React.FC = () => { // - Any of the modals are open. uploadTypeSelectorView || openCollectionSelector || - collectionNamerView || sidebarVisibilityProps.open || planSelectorVisibilityProps.open || fixCreationTimeVisibilityProps.open || exportVisibilityProps.open || authenticateUserVisibilityProps.open || + albumNameInputVisibilityProps.open || isFileViewerOpen ) { return; @@ -522,7 +522,7 @@ const Page: React.FC = () => { setTimeout(hideLoadingBar, 0); }, [showLoadingBar, hideLoadingBar]); - const handleFileAndCollectionSyncWithRemote = useCallback(async () => { + const fileAndCollectionSyncWithRemote = useCallback(async () => { const didUpdateFiles = await syncCollectionAndFiles({ onSetCollections: ( collections, @@ -551,27 +551,39 @@ const Page: React.FC = () => { } }, []); - const handleSyncWithRemote = useCallback( - async (force = false, silent = false) => { + const syncWithRemote = useCallback( + async (opts?: SyncWithRemoteOpts) => { + const { force, silent } = opts ?? {}; + + // Pre-flight checks. if (!navigator.onLine) return; - if (syncInProgress.current && !force) { - resync.current = { force, silent }; + if (await isSessionInvalid()) { + showSessionExpiredDialog(); return; } - const isForced = syncInProgress.current && force; - syncInProgress.current = true; - try { - const token = getToken(); - if (!token) { + if (!(await masterKeyFromSessionIfLoggedIn())) { + clearSessionStorage(); + router.push("/credentials"); + return; + } + + // Start or enqueue. + let isForced = false; + if (isSyncing.current) { + if (force) { + isForced = true; + } else { + resyncOpts.current = { force, silent }; return; } - const tokenValid = await isTokenValid(token); - if (!tokenValid) { - throw new Error(CustomError.SESSION_EXPIRED); - } - !silent && showLoadingBar(); + } + + // The sync + isSyncing.current = true; + try { + if (!silent) showLoadingBar(); await preCollectionAndFilesSync(); - await handleFileAndCollectionSyncWithRemote(); + await fileAndCollectionSyncWithRemote(); // syncWithRemote is called with the force flag set to true before // doing an upload. So it is possible, say when resuming a pending // upload, that we get two syncWithRemotes happening in parallel. @@ -581,26 +593,17 @@ const Page: React.FC = () => { await postCollectionAndFilesSync(); } } catch (e) { - switch (e.message) { - case CustomError.SESSION_EXPIRED: - showSessionExpiredDialog(); - break; - case CustomError.KEY_MISSING: - clearKeys(); - router.push("/credentials"); - break; - default: - log.error("syncWithRemote failed", e); - } + log.error("syncWithRemote failed", e); } finally { dispatch({ type: "clearUnsyncedState" }); - !silent && hideLoadingBar(); + if (!silent) hideLoadingBar(); } - syncInProgress.current = false; - if (resync.current) { - const { force, silent } = resync.current; - setTimeout(() => handleSyncWithRemote(force, silent), 0); - resync.current = undefined; + isSyncing.current = false; + + const nextOpts = resyncOpts.current; + if (nextOpts) { + resyncOpts.current = undefined; + setTimeout(() => syncWithRemote(nextOpts), 0); } }, [ @@ -608,13 +611,10 @@ const Page: React.FC = () => { hideLoadingBar, router, showSessionExpiredDialog, - handleFileAndCollectionSyncWithRemote, + fileAndCollectionSyncWithRemote, ], ); - // Alias for existing code. - const syncWithRemote = handleSyncWithRemote; - const setupSelectAllKeyBoardShortcutHandler = () => { const handleKeyUp = (e: KeyboardEvent) => { switch (e.key) { @@ -688,7 +688,7 @@ const Page: React.FC = () => { ); } clearSelection(); - await syncWithRemote(false, true); + await syncWithRemote({ silent: true }); } catch (e) { onGenericError(e); } finally { @@ -724,7 +724,7 @@ const Page: React.FC = () => { ); } clearSelection(); - await syncWithRemote(false, true); + await syncWithRemote({ silent: true }); } catch (e) { onGenericError(e); } finally { @@ -732,26 +732,26 @@ const Page: React.FC = () => { } }; - const showCreateCollectionModal = (op: CollectionOp) => { - const callback = async (collectionName: string) => { - try { - showLoadingBar(); - const collection = await createAlbum(collectionName); - await collectionOpsHelper(op)(collection); - } catch (e) { - onGenericError(e); - } finally { - hideLoadingBar(); - } - }; - return () => - setCollectionNamerAttributes({ - title: t("new_album"), - buttonText: t("create"), - autoFilledName: "", - callback, + const handleCreateAlbumForOp = useCallback( + (op: CollectionOp) => { + setPostCreateAlbumOp(op); + return showAlbumNameInput; + }, + [showAlbumNameInput], + ); + + const handleAlbumNameSubmit = useCallback( + async (name: string) => { + const collection = await createAlbum(name); + setPostCreateAlbumOp((postCreateAlbumOp) => { + // collectionOpsHelper does its own progress and error + // reporting, defer to that. + void collectionOpsHelper(postCreateAlbumOp!)(collection); + return undefined; }); - }; + }, + [collectionOpsHelper], + ); const handleSelectSearchOption = ( searchOption: SearchOption | undefined, @@ -781,9 +781,7 @@ const Page: React.FC = () => { }; const openUploader = (intent?: UploadTypeSelectorIntent) => { - if (!uploadManager.shouldAllowNewUpload()) { - return; - } + if (uploadManager.isUploadInProgress()) return; setUploadTypeSelectorView(true); setUploadTypeSelectorIntent(intent ?? "upload"); }; @@ -897,7 +895,8 @@ const Page: React.FC = () => { value={{ ...defaultGalleryContext, setActiveCollectionID: handleSetActiveCollectionID, - syncWithRemote, + syncWithRemote: (force, silent) => + syncWithRemote({ force, silent }), setBlockingLoad, photoListHeader, userIDToEmailMap, @@ -923,11 +922,6 @@ const Page: React.FC = () => { {...planSelectorVisibilityProps} setLoading={(v) => setBlockingLoad(v)} /> - { { activeCollection, activeCollectionID, activePerson, - setCollectionNamerAttributes, setPhotoListHeader, setFilesDownloadProgressAttributesCreator, filesDownloadProgressAttributesList, @@ -1047,7 +1038,9 @@ const Page: React.FC = () => { + syncWithRemote({ force, silent }) + } closeUploadTypeSelector={setUploadTypeSelectorView.bind( null, false, @@ -1055,7 +1048,6 @@ const Page: React.FC = () => { onOpenCollectionSelector={handleOpenCollectionSelector} onCloseCollectionSelector={handleCloseCollectionSelector} setLoading={setBlockingLoad} - setCollectionNamerAttributes={setCollectionNamerAttributes} setShouldDisableDropzone={setShouldDisableDropzone} onUploadFile={(file) => dispatch({ type: "uploadNormalFile", file }) @@ -1087,7 +1079,10 @@ const Page: React.FC = () => { !normalFiles?.length && !hiddenFiles?.length && activeCollectionID === ALL_SECTION ? ( - + ) : !isInSearchMode && !isFirstLoad && state.view.type == "people" && @@ -1131,7 +1126,7 @@ const Page: React.FC = () => { } onMarkTempDeleted={handleMarkTempDeleted} onSetOpenFileViewer={setIsFileViewerOpen} - onSyncWithRemote={handleSyncWithRemote} + onSyncWithRemote={syncWithRemote} onVisualFeedback={handleVisualFeedback} onSelectCollection={handleSelectCollection} onSelectPerson={handleSelectPerson} @@ -1139,12 +1134,21 @@ const Page: React.FC = () => { )} + ); @@ -1208,7 +1212,7 @@ const SidebarButton: React.FC = ({ onClick }) => ( ); const UploadButton: React.FC = ({ onClick }) => { - const disabled = !uploadManager.shouldAllowNewUpload(); + const disabled = uploadManager.isUploadInProgress(); const isSmallWidth = useIsSmallWidth(); const icon = ; @@ -1241,16 +1245,15 @@ const HiddenSectionNavbarContents: React.FC< direction="row" sx={(theme) => ({ gap: "24px", - width: "100%", + flex: 1, + alignItems: "center", background: theme.vars.palette.background.default, })} > - - {t("section_hidden")} - + {t("section_hidden")} ); diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 6121f9ad51..f68ad9c08d 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -15,7 +15,11 @@ import { AccountsPageContents, AccountsPageTitle, } from "ente-accounts/components/layouts/centered-paper"; -import { SpacedRow, Stack100vhCenter } from "ente-base/components/containers"; +import { + CenteredFill, + SpacedRow, + Stack100vhCenter, +} from "ente-base/components/containers"; import { EnteLogo } from "ente-base/components/EnteLogo"; import { LoadingIndicator, @@ -28,6 +32,10 @@ import { OverflowMenu, OverflowMenuOption, } from "ente-base/components/OverflowMenu"; +import { + SingleInputForm, + type SingleInputFormProps, +} from "ente-base/components/SingleInputForm"; import { useIsSmallWidth, useIsTouchscreen, @@ -52,10 +60,6 @@ import { } from "ente-new/photos/services/collection"; import { sortFiles } from "ente-new/photos/services/files"; import { usePhotosAppContext } from "ente-new/photos/types/context"; -import { CenteredFlex } from "ente-shared/components/Container"; -import SingleInputForm, { - type SingleInputFormProps, -} from "ente-shared/components/SingleInputForm"; import { CustomError, parseSharingErrorCodes } from "ente-shared/error"; import { t } from "i18next"; import { useRouter } from "next/router"; @@ -275,9 +279,9 @@ export default function PublicCollectionGallery() { onAddPhotos ? { item: ( - + - + ), height: 104, } @@ -285,7 +289,7 @@ export default function PublicCollectionGallery() { ); }, [onAddPhotos]); - const handleSyncWithRemote = useCallback(async () => { + const syncWithRemote = useCallback(async () => { const collectionUID = getPublicCollectionUID( credentials.current.accessToken, ); @@ -367,16 +371,13 @@ export default function PublicCollectionGallery() { } }, [showLoadingBar, hideLoadingBar]); - // TODO: See gallery - const syncWithRemote = handleSyncWithRemote; - // See: [Note: Visual feedback to acknowledge user actions] const handleVisualFeedback = useCallback(() => { showLoadingBar(); setTimeout(hideLoadingBar, 0); }, [showLoadingBar, hideLoadingBar]); - const verifyLinkPassword: SingleInputFormProps["callback"] = async ( + const handleSubmitPassword: SingleInputFormProps["onSubmit"] = async ( password, setFieldError, ) => { @@ -396,10 +397,9 @@ export default function PublicCollectionGallery() { log.error("Failed to verifyLinkPassword", e); if (isHTTP401Error(e)) { setFieldError(t("incorrect_password")); - } else { - setFieldError(t("generic_error_retry")); + return; } - return; + throw e; } await syncWithRemote(); @@ -456,10 +456,10 @@ export default function PublicCollectionGallery() { {t("link_password_description")} @@ -523,7 +523,7 @@ export default function PublicCollectionGallery() { setFilesDownloadProgressAttributesCreator={ setFilesDownloadProgressAttributesCreator } - onSyncWithRemote={handleSyncWithRemote} + onSyncWithRemote={syncWithRemote} onVisualFeedback={handleVisualFeedback} /> {blockingLoad && } @@ -558,7 +558,7 @@ const EnteLogoLink = styled("a")(({ theme }) => ({ })); const AddPhotosButton: React.FC = ({ onClick }) => { - const disabled = !uploadManager.shouldAllowNewUpload(); + const disabled = uploadManager.isUploadInProgress(); const isSmallWidth = useIsSmallWidth(); const icon = ; @@ -585,7 +585,7 @@ const AddPhotosButton: React.FC = ({ onClick }) => { * shrink on mobile sized screens. */ const AddMorePhotosButton: React.FC = ({ onClick }) => { - const disabled = !uploadManager.shouldAllowNewUpload(); + const disabled = uploadManager.isUploadInProgress(); return ( { const collections = await getLocalCollections(); + const userID = ensureLocalUser().id; for (const collection of collections) { - if (collection.type == "favorites") { + // See: [Note: User and shared favorites] + if (collection.type == "favorites" && collection.owner.id == userID) { return collection; } } @@ -627,9 +633,7 @@ export const sortCollectionSummaries = ( return (b.updationTime ?? 0) - (a.updationTime ?? 0); } }) - // TODO: - // eslint-disable-next-line no-constant-binary-expression - .sort((a, b) => b.order ?? 0 - a.order ?? 0) + .sort((a, b) => (b.order ?? 0) - (a.order ?? 0)) .sort( (a, b) => CollectionSummaryOrder.get(a.type) - diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts deleted file mode 100644 index 527023a5cf..0000000000 --- a/web/apps/photos/src/services/export/index.ts +++ /dev/null @@ -1,1516 +0,0 @@ -import { ensureElectron } from "ente-base/electron"; -import { joinPath } from "ente-base/file-name"; -import log from "ente-base/log"; -import { downloadManager } from "ente-gallery/services/download"; -import { writeStream } from "ente-gallery/utils/native-stream"; -import type { Collection } from "ente-media/collection"; -import { mergeMetadata, type EnteFile } from "ente-media/file"; -import { fileLocation, type Metadata } from "ente-media/file-metadata"; -import { FileType } from "ente-media/file-type"; -import { decodeLivePhoto } from "ente-media/live-photo"; -import { - createCollectionNameByID, - getCollectionUserFacingName, -} from "ente-new/photos/services/collection"; -import { getAllLocalCollections } from "ente-new/photos/services/collections"; -import { - exportMetadataDirectoryName, - exportTrashDirectoryName, -} from "ente-new/photos/services/export"; -import { getAllLocalFiles } from "ente-new/photos/services/files"; -import { - safeDirectoryName, - safeFileName, -} from "ente-new/photos/utils/native-fs"; -import { CustomError } from "ente-shared/error"; -import { getData, setData } from "ente-shared/storage/localStorage"; -import { PromiseQueue } from "ente-utils/promise"; -import i18n from "i18next"; -import { migrateExport, type ExportRecord } from "./migration"; - -/** Name of the JSON file in which we keep the state of the export. */ -const exportRecordFileName = "export_status.json"; - -/** - * Name of the top level directory which we create underneath the selected - * directory when the user starts an export to the file system. - */ -const exportDirectoryName = "Ente Photos"; - -export const ExportStage = { - init: 0, - migration: 1, - starting: 2, - exportingFiles: 3, - trashingDeletedFiles: 4, - renamingCollectionFolders: 5, - trashingDeletedCollections: 6, - finished: 7, -} as const; - -export type ExportStage = (typeof ExportStage)[keyof typeof ExportStage]; - -export interface ExportProgress { - success: number; - failed: number; - total: number; -} - -export interface ExportSettings { - folder: string; - continuousExport: boolean; -} - -export type CollectionExportNames = Record; - -export type FileExportNames = Record; - -export const NULL_EXPORT_RECORD: ExportRecord = { - version: 3, - lastAttemptTimestamp: null, - stage: ExportStage.init, - fileExportNames: {}, - collectionExportNames: {}, -}; - -export interface ExportOpts { - /** - * If true, perform an additional on-disk check to determine which files - * need to be exported. - * - * This has performance implications for huge libraries, so we only do this: - * - For the first export after an app start - * - If the user explicitly presses the "Resync" button. - */ - resync?: boolean; -} - -interface ExportUIUpdaters { - setExportStage: (stage: ExportStage) => void; - setExportProgress: (progress: ExportProgress) => void; - setLastExportTime: (exportTime: number) => void; - setPendingExports: (pendingExports: EnteFile[]) => void; -} - -interface RequestCanceller { - exec: () => void; -} - -interface CancellationStatus { - status: boolean; -} - -class ExportService { - private exportSettings: ExportSettings; - private exportInProgress: RequestCanceller = null; - private resync = true; - private reRunNeeded = false; - private exportRecordUpdater = new PromiseQueue(); - private continuousExportEventHandler: () => void; - private uiUpdater: ExportUIUpdaters = { - setExportProgress: () => {}, - setExportStage: () => {}, - setLastExportTime: () => {}, - setPendingExports: () => {}, - }; - private currentExportProgress: ExportProgress = { - total: 0, - success: 0, - failed: 0, - }; - private cachedMetadataDateTimeFormatter: Intl.DateTimeFormat; - - getExportSettings(): ExportSettings { - try { - if (this.exportSettings) { - return this.exportSettings; - } - const exportSettings = getData("export"); - this.exportSettings = exportSettings; - return exportSettings; - } catch (e) { - log.error("getExportSettings failed", e); - throw e; - } - } - - updateExportSettings(newData: Partial) { - try { - const exportSettings = this.getExportSettings(); - const newSettings = { ...exportSettings, ...newData }; - this.exportSettings = newSettings; - setData("export", newSettings); - } catch (e) { - log.error("updateExportSettings failed", e); - throw e; - } - } - - async runMigration( - exportDir: string, - exportRecord: ExportRecord, - updateProgress: (progress: ExportProgress) => void, - ) { - try { - log.info("running migration"); - await migrateExport(exportDir, exportRecord, updateProgress); - log.info("migration completed"); - } catch (e) { - log.error("migration failed", e); - throw e; - } - } - - setUIUpdaters(uiUpdater: ExportUIUpdaters) { - this.uiUpdater = uiUpdater; - this.uiUpdater.setExportProgress(this.currentExportProgress); - } - - private updateExportProgress(exportProgress: ExportProgress) { - this.currentExportProgress = exportProgress; - this.uiUpdater.setExportProgress(exportProgress); - } - - private async updateExportStage(stage: ExportStage) { - const exportFolder = this.getExportSettings()?.folder; - await this.updateExportRecord(exportFolder, { stage }); - this.uiUpdater.setExportStage(stage); - } - - private async updateLastExportTime(exportTime: number) { - const exportFolder = this.getExportSettings()?.folder; - await this.updateExportRecord(exportFolder, { - lastAttemptTimestamp: exportTime, - }); - this.uiUpdater.setLastExportTime(exportTime); - } - - private resyncOnce() { - const resync = this.resync; - this.resync = false; - return resync; - } - - resumeExport() { - this.scheduleExport({ resync: this.resyncOnce() }); - } - - enableContinuousExport() { - if (this.continuousExportEventHandler) { - log.warn("Continuous export already enabled"); - return; - } - this.continuousExportEventHandler = () => { - this.scheduleExport({ resync: this.resyncOnce() }); - }; - this.continuousExportEventHandler(); - } - - disableContinuousExport() { - if (!this.continuousExportEventHandler) { - log.warn("Continuous export already disabled"); - return; - } - this.continuousExportEventHandler = null; - } - - /** - * Called when the local database of files changes. - */ - onLocalFilesUpdated() { - if (this.continuousExportEventHandler) { - this.continuousExportEventHandler(); - } - } - - getPendingExports = async ( - exportRecord: ExportRecord, - ): Promise => { - try { - const files = await getAllLocalFiles(); - - const unExportedFiles = getUnExportedFiles( - files, - exportRecord, - undefined, - ); - return unExportedFiles; - } catch (e) { - log.error("getUpdateFileLists failed", e); - throw e; - } - }; - - async preExport(exportFolder: string) { - await this.verifyExportFolderExists(exportFolder); - const exportRecord = await this.getExportRecord(exportFolder); - await this.updateExportStage(ExportStage.migration); - await this.runMigration( - exportFolder, - exportRecord, - this.updateExportProgress.bind(this), - ); - await this.updateExportStage(ExportStage.starting); - } - - async postExport() { - try { - const exportFolder = this.getExportSettings()?.folder; - if (!(await this.exportFolderExists(exportFolder))) { - this.uiUpdater.setExportStage(ExportStage.init); - return; - } - await this.updateExportStage(ExportStage.finished); - await this.updateLastExportTime(Date.now()); - - const exportRecord = await this.getExportRecord(exportFolder); - - const pendingExports = await this.getPendingExports(exportRecord); - this.uiUpdater.setPendingExports(pendingExports); - } catch (e) { - log.error("postExport failed", e); - } - } - - async stopRunningExport() { - try { - log.info("user requested export cancellation"); - this.exportInProgress.exec(); - this.exportInProgress = null; - this.reRunNeeded = false; - await this.postExport(); - } catch (e) { - log.error("stopRunningExport failed", e); - } - } - - scheduleExport = async (exportOpts: ExportOpts) => { - try { - if (this.exportInProgress) { - log.info("export in progress, scheduling re-run"); - this.reRunNeeded = true; - return; - } else { - log.info("export not in progress, starting export"); - } - - const isCanceled: CancellationStatus = { status: false }; - const canceller: RequestCanceller = { - exec: () => { - isCanceled.status = true; - }, - }; - this.exportInProgress = canceller; - try { - const exportFolder = this.getExportSettings()?.folder; - await this.preExport(exportFolder); - log.info("export started"); - await this.runExport(exportFolder, isCanceled, exportOpts); - log.info("export completed"); - } finally { - if (isCanceled.status) { - log.info("export cancellation done"); - if (!this.exportInProgress) { - await this.postExport(); - } - } else { - await this.postExport(); - log.info("resetting export in progress after completion"); - this.exportInProgress = null; - if (this.reRunNeeded) { - this.reRunNeeded = false; - log.info("re-running export"); - setTimeout(() => this.scheduleExport(exportOpts), 0); - } - } - } - } catch (e) { - if ( - e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && - e.message !== CustomError.EXPORT_STOPPED - ) { - log.error("scheduleExport failed", e); - } - } - }; - - private async runExport( - exportFolder: string, - isCanceled: CancellationStatus, - { resync }: ExportOpts, - ) { - try { - const files = mergeMetadata(await getAllLocalFiles()); - const collections = await getAllLocalCollections(); - - const exportRecord = await this.getExportRecord(exportFolder); - const collectionIDExportNameMap = - convertCollectionIDExportNameObjectToMap( - exportRecord.collectionExportNames, - ); - const collectionIDNameMap = createCollectionNameByID(collections); - - const renamedCollections = getRenamedExportedCollections( - collections, - exportRecord, - ); - - const removedFileUIDs = getDeletedExportedFiles( - files, - exportRecord, - ); - - const diskFileRecordIDs = resync - ? await readOnDiskFileExportRecordIDs( - files, - collectionIDExportNameMap, - exportFolder, - exportRecord, - isCanceled, - ) - : undefined; - - const filesToExport = getUnExportedFiles( - files, - exportRecord, - diskFileRecordIDs, - ); - - const deletedExportedCollections = getDeletedExportedCollections( - collections, - exportRecord, - ); - - log.info( - `[export] files: ${files.length}, disk files: ${diskFileRecordIDs?.size ?? ""}, unexported files: ${filesToExport.length}, deleted exported files: ${removedFileUIDs.length}, renamed collections: ${renamedCollections.length}, deleted collections: ${deletedExportedCollections.length}`, - ); - let success = 0; - let failed = 0; - this.uiUpdater.setExportProgress({ - success: success, - failed: failed, - total: filesToExport.length, - }); - const incrementSuccess = () => { - this.updateExportProgress({ - success: ++success, - failed: failed, - total: filesToExport.length, - }); - }; - const incrementFailed = () => { - this.updateExportProgress({ - success: success, - failed: ++failed, - total: filesToExport.length, - }); - }; - if (renamedCollections?.length > 0) { - this.updateExportStage(ExportStage.renamingCollectionFolders); - log.info(`renaming ${renamedCollections.length} collections`); - await this.collectionRenamer( - exportFolder, - collectionIDExportNameMap, - renamedCollections, - isCanceled, - ); - } - - if (removedFileUIDs?.length > 0) { - this.updateExportStage(ExportStage.trashingDeletedFiles); - log.info(`trashing ${removedFileUIDs.length} files`); - await this.fileTrasher( - exportFolder, - collectionIDExportNameMap, - removedFileUIDs, - isCanceled, - ); - } - if (filesToExport?.length > 0) { - this.updateExportStage(ExportStage.exportingFiles); - log.info(`exporting ${filesToExport.length} files`); - await this.fileExporter( - filesToExport, - collectionIDNameMap, - collectionIDExportNameMap, - exportFolder, - incrementSuccess, - incrementFailed, - isCanceled, - ); - } - if (deletedExportedCollections?.length > 0) { - this.updateExportStage(ExportStage.trashingDeletedCollections); - log.info( - `removing ${deletedExportedCollections.length} collections`, - ); - await this.collectionRemover( - deletedExportedCollections, - exportFolder, - isCanceled, - ); - } - } catch (e) { - if ( - e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && - e.message !== CustomError.EXPORT_STOPPED - ) { - log.error("runExport failed", e); - } - throw e; - } - } - - async collectionRenamer( - exportFolder: string, - collectionIDExportNameMap: Map, - renamedCollections: Collection[], - isCanceled: CancellationStatus, - ) { - const fs = ensureElectron().fs; - try { - for (const collection of renamedCollections) { - try { - if (isCanceled.status) { - throw Error(CustomError.EXPORT_STOPPED); - } - await this.verifyExportFolderExists(exportFolder); - const oldCollectionExportName = - collectionIDExportNameMap.get(collection.id); - const oldCollectionExportPath = joinPath( - exportFolder, - oldCollectionExportName, - ); - const newCollectionExportName = await safeDirectoryName( - exportFolder, - getCollectionUserFacingName(collection), - fs.exists, - ); - log.info( - `renaming collection with id ${collection.id} from ${oldCollectionExportName} to ${newCollectionExportName}`, - ); - const newCollectionExportPath = joinPath( - exportFolder, - newCollectionExportName, - ); - await this.addCollectionExportedRecord( - exportFolder, - collection.id, - newCollectionExportName, - ); - collectionIDExportNameMap.set( - collection.id, - newCollectionExportName, - ); - try { - await fs.rename( - oldCollectionExportPath, - newCollectionExportPath, - ); - } catch (e) { - await this.addCollectionExportedRecord( - exportFolder, - collection.id, - oldCollectionExportName, - ); - collectionIDExportNameMap.set( - collection.id, - oldCollectionExportName, - ); - throw e; - } - log.info( - `renaming collection with id ${collection.id} from ${oldCollectionExportName} to ${newCollectionExportName} successful`, - ); - } catch (e) { - log.error("collectionRenamer failed a collection", e); - if ( - e.message === - CustomError.UPDATE_EXPORTED_RECORD_FAILED || - e.message === - CustomError.EXPORT_FOLDER_DOES_NOT_EXIST || - e.message === CustomError.EXPORT_STOPPED - ) { - throw e; - } - } - } - } catch (e) { - if ( - e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && - e.message !== CustomError.EXPORT_STOPPED - ) { - log.error("collectionRenamer failed", e); - } - throw e; - } - } - - async collectionRemover( - deletedExportedCollectionIDs: number[], - exportFolder: string, - isCanceled: CancellationStatus, - ) { - const fs = ensureElectron().fs; - const rmdirIfExists = async (dirPath: string) => { - if (await fs.exists(dirPath)) await fs.rmdir(dirPath); - }; - try { - const exportRecord = await this.getExportRecord(exportFolder); - const collectionIDPathMap = - convertCollectionIDExportNameObjectToMap( - exportRecord.collectionExportNames, - ); - for (const collectionID of deletedExportedCollectionIDs) { - try { - if (isCanceled.status) { - throw Error(CustomError.EXPORT_STOPPED); - } - await this.verifyExportFolderExists(exportFolder); - log.info( - `removing collection with id ${collectionID} from export folder`, - ); - const collectionExportName = - collectionIDPathMap.get(collectionID); - // verify that the all exported files from the collection has been removed - const collectionExportedFiles = getCollectionExportedFiles( - exportRecord, - collectionID, - ); - if (collectionExportedFiles.length > 0) { - throw new Error( - "collection is not empty, can't remove", - ); - } - const collectionExportPath = joinPath( - exportFolder, - collectionExportName, - ); - await this.removeCollectionExportedRecord( - exportFolder, - collectionID, - ); - try { - // delete the collection metadata folder - await rmdirIfExists( - getMetadataFolderExportPath(collectionExportPath), - ); - // delete the collection folder - await rmdirIfExists(collectionExportPath); - } catch (e) { - await this.addCollectionExportedRecord( - exportFolder, - collectionID, - collectionExportName, - ); - throw e; - } - log.info( - `removing collection with id ${collectionID} from export folder successful`, - ); - } catch (e) { - log.error("collectionRemover failed a collection", e); - if ( - e.message === - CustomError.UPDATE_EXPORTED_RECORD_FAILED || - e.message === - CustomError.EXPORT_FOLDER_DOES_NOT_EXIST || - e.message === CustomError.EXPORT_STOPPED - ) { - throw e; - } - } - } - } catch (e) { - if ( - e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && - e.message !== CustomError.EXPORT_STOPPED - ) { - log.error("collectionRemover failed", e); - } - throw e; - } - } - - async fileExporter( - files: EnteFile[], - collectionIDNameMap: Map, - collectionIDFolderNameMap: Map, - exportDir: string, - incrementSuccess: () => void, - incrementFailed: () => void, - isCanceled: CancellationStatus, - ): Promise { - const fs = ensureElectron().fs; - try { - for (const file of files) { - log.info( - `exporting file ${file.metadata.title} with id ${ - file.id - } from collection ${collectionIDNameMap.get( - file.collectionID, - )}`, - ); - if (isCanceled.status) { - throw Error(CustomError.EXPORT_STOPPED); - } - try { - await this.verifyExportFolderExists(exportDir); - let collectionExportName = collectionIDFolderNameMap.get( - file.collectionID, - ); - if (!collectionExportName) { - collectionExportName = - await this.createNewCollectionExport( - exportDir, - file.collectionID, - collectionIDNameMap, - ); - await this.addCollectionExportedRecord( - exportDir, - file.collectionID, - collectionExportName, - ); - collectionIDFolderNameMap.set( - file.collectionID, - collectionExportName, - ); - } - const collectionExportPath = joinPath( - exportDir, - collectionExportName, - ); - await fs.mkdirIfNeeded(collectionExportPath); - await fs.mkdirIfNeeded( - getMetadataFolderExportPath(collectionExportPath), - ); - await this.downloadAndSave( - exportDir, - collectionExportPath, - file, - ); - incrementSuccess(); - log.info( - `exporting file ${file.metadata.title} with id ${ - file.id - } from collection ${collectionIDNameMap.get( - file.collectionID, - )} successful`, - ); - } catch (e) { - incrementFailed(); - log.error("export failed for a file", e); - if ( - e.message === - CustomError.UPDATE_EXPORTED_RECORD_FAILED || - e.message === - CustomError.EXPORT_FOLDER_DOES_NOT_EXIST || - e.message === CustomError.EXPORT_STOPPED - ) { - throw e; - } - } - } - } catch (e) { - if ( - e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && - e.message !== CustomError.EXPORT_STOPPED - ) { - log.error("fileExporter failed", e); - } - throw e; - } - } - - async fileTrasher( - exportDir: string, - collectionIDExportNameMap: Map, - removedFileUIDs: string[], - isCanceled: CancellationStatus, - ): Promise { - try { - const exportRecord = await this.getExportRecord(exportDir); - const fileIDExportNameMap = convertFileIDExportNameObjectToMap( - exportRecord.fileExportNames, - ); - for (const fileUID of removedFileUIDs) { - await this.verifyExportFolderExists(exportDir); - log.info(`trashing file with id ${fileUID}`); - if (isCanceled.status) { - throw Error(CustomError.EXPORT_STOPPED); - } - try { - const fileExportName = fileIDExportNameMap.get(fileUID); - const collectionID = getCollectionIDFromFileUID(fileUID); - const collectionExportName = - collectionIDExportNameMap.get(collectionID); - - if (isLivePhotoExportName(fileExportName)) { - const { image, video } = - parseLivePhotoExportName(fileExportName); - - await moveToFSTrash( - exportDir, - collectionExportName, - image, - ); - - await moveToFSTrash( - exportDir, - collectionExportName, - video, - ); - } else { - await moveToFSTrash( - exportDir, - collectionExportName, - fileExportName, - ); - } - - await this.removeFileExportedRecord(exportDir, fileUID); - - log.info(`Moved file id ${fileUID} to Trash`); - } catch (e) { - log.error("trashing failed for a file", e); - if ( - e.message === - CustomError.UPDATE_EXPORTED_RECORD_FAILED || - e.message === - CustomError.EXPORT_FOLDER_DOES_NOT_EXIST || - e.message === CustomError.EXPORT_STOPPED - ) { - throw e; - } - } - } - } catch (e) { - if ( - e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && - e.message !== CustomError.EXPORT_STOPPED - ) { - log.error("fileTrasher failed", e); - } - throw e; - } - } - - async addFileExportedRecord( - folder: string, - fileUID: string, - fileExportName: string, - ) { - try { - const exportRecord = await this.getExportRecord(folder); - if (!exportRecord.fileExportNames) { - exportRecord.fileExportNames = {}; - } - exportRecord.fileExportNames = { - ...exportRecord.fileExportNames, - [fileUID]: fileExportName, - }; - await this.updateExportRecord(folder, exportRecord); - } catch (e) { - if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { - log.error("addFileExportedRecord failed", e); - } - throw e; - } - } - - async addCollectionExportedRecord( - folder: string, - collectionID: number, - collectionExportName: string, - ) { - try { - const exportRecord = await this.getExportRecord(folder); - if (!exportRecord?.collectionExportNames) { - exportRecord.collectionExportNames = {}; - } - exportRecord.collectionExportNames = { - ...exportRecord.collectionExportNames, - [collectionID]: collectionExportName, - }; - - await this.updateExportRecord(folder, exportRecord); - } catch (e) { - if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { - log.error("addCollectionExportedRecord failed", e); - } - throw e; - } - } - - async removeCollectionExportedRecord(folder: string, collectionID: number) { - try { - const exportRecord = await this.getExportRecord(folder); - - exportRecord.collectionExportNames = Object.fromEntries( - Object.entries(exportRecord.collectionExportNames).filter( - ([key]) => key !== collectionID.toString(), - ), - ); - - await this.updateExportRecord(folder, exportRecord); - } catch (e) { - if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { - log.error("removeCollectionExportedRecord failed", e); - } - throw e; - } - } - - async removeFileExportedRecord(folder: string, fileUID: string) { - try { - const exportRecord = await this.getExportRecord(folder); - exportRecord.fileExportNames = Object.fromEntries( - Object.entries(exportRecord.fileExportNames).filter( - ([key]) => key !== fileUID, - ), - ); - await this.updateExportRecord(folder, exportRecord); - } catch (e) { - if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { - log.error("removeFileExportedRecord failed", e); - } - throw e; - } - } - - async updateExportRecord(folder: string, newData: Partial) { - return this.exportRecordUpdater.add(() => - this.updateExportRecordHelper(folder, newData), - ); - } - - async updateExportRecordHelper( - folder: string, - newData: Partial, - ) { - try { - const exportRecord = await this.getExportRecord(folder); - const newRecord: ExportRecord = { ...exportRecord, ...newData }; - await ensureElectron().fs.writeFileViaBackup( - joinPath(folder, exportRecordFileName), - JSON.stringify(newRecord, null, 2), - ); - return newRecord; - } catch (e) { - if (e.message === CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { - throw e; - } - log.error("error updating Export Record", e); - throw Error(CustomError.UPDATE_EXPORTED_RECORD_FAILED); - } - } - - async getExportRecord(folder: string): Promise { - const electron = ensureElectron(); - const fs = electron.fs; - try { - await this.verifyExportFolderExists(folder); - const exportRecordJSONPath = joinPath(folder, exportRecordFileName); - if (!(await fs.exists(exportRecordJSONPath))) { - return await this.createEmptyExportRecord(exportRecordJSONPath); - } - const recordFile = await fs.readTextFile(exportRecordJSONPath); - return JSON.parse(recordFile); - } catch (e) { - if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { - log.error("export Record JSON parsing failed", e); - } - throw e; - } - } - - async createNewCollectionExport( - exportFolder: string, - collectionID: number, - collectionIDNameMap: Map, - ) { - const fs = ensureElectron().fs; - await this.verifyExportFolderExists(exportFolder); - const collectionName = collectionIDNameMap.get(collectionID); - const collectionExportName = await safeDirectoryName( - exportFolder, - collectionName, - fs.exists, - ); - const collectionExportPath = joinPath( - exportFolder, - collectionExportName, - ); - await fs.mkdirIfNeeded(collectionExportPath); - await fs.mkdirIfNeeded( - getMetadataFolderExportPath(collectionExportPath), - ); - - return collectionExportName; - } - - async downloadAndSave( - exportDir: string, - collectionExportPath: string, - file: EnteFile, - ): Promise { - const electron = ensureElectron(); - try { - const fileUID = getExportRecordFileUID(file); - const originalFileStream = await downloadManager.fileStream(file); - if (file.metadata.fileType === FileType.livePhoto) { - await this.exportLivePhoto( - exportDir, - fileUID, - collectionExportPath, - originalFileStream, - file, - ); - } else { - const fileExportName = await safeFileName( - collectionExportPath, - file.metadata.title, - electron.fs.exists, - ); - await this.saveMetadataFile( - collectionExportPath, - fileExportName, - file, - ); - await writeStream( - electron, - joinPath(collectionExportPath, fileExportName), - originalFileStream, - ); - await this.addFileExportedRecord( - exportDir, - fileUID, - fileExportName, - ); - } - } catch (e) { - log.error("download and save failed", e); - throw e; - } - } - - private async exportLivePhoto( - exportDir: string, - fileUID: string, - collectionExportPath: string, - fileStream: ReadableStream, - file: EnteFile, - ) { - const fs = ensureElectron().fs; - const fileBlob = await new Response(fileStream).blob(); - const livePhoto = await decodeLivePhoto(file.metadata.title, fileBlob); - const imageExportName = await safeFileName( - collectionExportPath, - livePhoto.imageFileName, - fs.exists, - ); - const videoExportName = await safeFileName( - collectionExportPath, - livePhoto.videoFileName, - fs.exists, - ); - - const livePhotoExportName = getLivePhotoExportName( - imageExportName, - videoExportName, - ); - - await this.saveMetadataFile( - collectionExportPath, - imageExportName, - file, - ); - await writeStream( - electron, - joinPath(collectionExportPath, imageExportName), - new Response(livePhoto.imageData).body, - ); - - await this.saveMetadataFile( - collectionExportPath, - videoExportName, - file, - ); - try { - await writeStream( - electron, - joinPath(collectionExportPath, videoExportName), - new Response(livePhoto.videoData).body, - ); - } catch (e) { - await fs.rm(joinPath(collectionExportPath, imageExportName)); - throw e; - } - - await this.addFileExportedRecord( - exportDir, - fileUID, - livePhotoExportName, - ); - } - - private async saveMetadataFile( - collectionExportPath: string, - fileExportName: string, - file: EnteFile, - ) { - const formatter = this.metadataDateTimeFormatter(); - await ensureElectron().fs.writeFile( - getFileMetadataExportPath(collectionExportPath, fileExportName), - getGoogleLikeMetadataFile(fileExportName, file, formatter), - ); - } - - /** - * Lazily created, cached instance of the date time formatter that should be - * used for formatting the dates added to the metadata file. - */ - private metadataDateTimeFormatter() { - if (this.cachedMetadataDateTimeFormatter) - return this.cachedMetadataDateTimeFormatter; - - // AFAIK, Google's format is not documented. It also seems to vary with - // locale. This is a best attempt at constructing a formatter that - // mirrors the format used by the timestamps in the takeout JSON. - const formatter = new Intl.DateTimeFormat(i18n.language, { - month: "short", - day: "numeric", - year: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - timeZoneName: "short", - timeZone: "UTC", - }); - this.cachedMetadataDateTimeFormatter = formatter; - return formatter; - } - - isExportInProgress = () => { - return this.exportInProgress; - }; - - exportFolderExists = async (exportFolder: string) => { - return exportFolder && (await ensureElectron().fs.exists(exportFolder)); - }; - - private verifyExportFolderExists = async (exportFolder: string) => { - try { - if (!(await this.exportFolderExists(exportFolder))) { - throw Error(CustomError.EXPORT_FOLDER_DOES_NOT_EXIST); - } - } catch (e) { - if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { - log.error("verifyExportFolderExists failed", e); - } - throw e; - } - }; - - private createEmptyExportRecord = async (exportRecordJSONPath: string) => { - const exportRecord: ExportRecord = NULL_EXPORT_RECORD; - await ensureElectron().fs.writeFile( - exportRecordJSONPath, - JSON.stringify(exportRecord, null, 2), - ); - return exportRecord; - }; -} - -const exportService = new ExportService(); - -export default exportService; - -/** - * If there are any in-progress exports, or if continuous exports are enabled, - * resume them. - */ -export const resumeExportsIfNeeded = async () => { - const exportSettings = exportService.getExportSettings(); - if (!(await exportService.exportFolderExists(exportSettings?.folder))) { - return; - } - const exportRecord = await exportService.getExportRecord( - exportSettings.folder, - ); - if (exportSettings.continuousExport) { - exportService.enableContinuousExport(); - } - if (isExportInProgress(exportRecord.stage)) { - log.debug(() => "Resuming in-progress export"); - exportService.resumeExport(); - } -}; - -/** - * Prompt the user to select a directory and create an export directory in it. - * - * If the user cancels the selection, return undefined. - */ -export const selectAndPrepareExportDirectory = async (): Promise< - string | undefined -> => { - const electron = ensureElectron(); - - const rootDir = await electron.selectDirectory(); - if (!rootDir) return undefined; - - const exportDir = joinPath(rootDir, exportDirectoryName); - await electron.fs.mkdirIfNeeded(exportDir); - return exportDir; -}; - -export const getExportRecordFileUID = (file: EnteFile) => - `${file.id}_${file.collectionID}_${file.updationTime}`; - -export const getCollectionIDFromFileUID = (fileUID: string) => - Number(fileUID.split("_")[1]); - -const convertCollectionIDExportNameObjectToMap = ( - collectionExportNames: CollectionExportNames, -): Map => { - return new Map( - Object.entries(collectionExportNames ?? {}).map((e) => { - return [Number(e[0]), String(e[1])]; - }), - ); -}; - -const convertFileIDExportNameObjectToMap = ( - fileExportNames: FileExportNames, -): Map => { - return new Map( - Object.entries(fileExportNames ?? {}).map((e) => { - return [String(e[0]), String(e[1])]; - }), - ); -}; - -const getRenamedExportedCollections = ( - collections: Collection[], - exportRecord: ExportRecord, -) => { - if (!exportRecord?.collectionExportNames) { - return []; - } - const collectionIDExportNameMap = convertCollectionIDExportNameObjectToMap( - exportRecord.collectionExportNames, - ); - const renamedCollections = collections.filter((collection) => { - if (collectionIDExportNameMap.has(collection.id)) { - const currentExportName = collectionIDExportNameMap.get( - collection.id, - ); - - const collectionExportName = - getCollectionUserFacingName(collection); - - if (currentExportName === collectionExportName) { - return false; - } - const hasNumberedSuffix = /\(\d+\)$/.exec(currentExportName); - const currentExportNameWithoutNumberedSuffix = hasNumberedSuffix - ? currentExportName.replace(/\(\d+\)$/, "") - : currentExportName; - - return ( - collectionExportName !== currentExportNameWithoutNumberedSuffix - ); - } - return false; - }); - return renamedCollections; -}; - -const getDeletedExportedCollections = ( - collections: Collection[], - exportRecord: ExportRecord, -) => { - if (!exportRecord?.collectionExportNames) { - return []; - } - const presentCollections = new Set( - collections.map((collection) => collection.id), - ); - const deletedExportedCollections = Object.keys( - exportRecord?.collectionExportNames, - ) - .map(Number) - .filter((collectionID) => { - if (!presentCollections.has(collectionID)) { - return true; - } - return false; - }); - return deletedExportedCollections; -}; - -/** - * Return export record IDs of {@link files} for which there is also exists a - * file on disk. - */ -const readOnDiskFileExportRecordIDs = async ( - files: EnteFile[], - collectionIDFolderNameMap: Map, - exportDir: string, - exportRecord: ExportRecord, - isCanceled: CancellationStatus, -): Promise> => { - const fs = ensureElectron().fs; - - const result = new Set(); - if (!(await fs.exists(exportDir))) return result; - - // Both the paths involved are guaranteed to use POSIX separators and thus - // can directly be compared. - // - // - `exportDir` traces its origin to `electron.selectDirectory()`, which - // returns POSIX paths. Down below we use it as the base directory when - // constructing paths for the items to export. - // - // - `findFiles` is also guaranteed to return POSIX paths. - // - const ls = new Set(await ensureElectron().fs.findFiles(exportDir)); - - const fileExportNames = exportRecord.fileExportNames ?? {}; - - for (const file of files) { - if (isCanceled.status) throw Error(CustomError.EXPORT_STOPPED); - - const collectionExportName = collectionIDFolderNameMap.get( - file.collectionID, - ); - if (!collectionExportName) continue; - - const collectionExportPath = joinPath(exportDir, collectionExportName); - const recordID = getExportRecordFileUID(file); - const exportName = fileExportNames[recordID]; - if (!exportName) continue; - - if (ls.has(joinPath(collectionExportPath, exportName))) { - result.add(recordID); - } else { - // It might be a live photo - these store a JSON string instead of - // the file's name as the exportName. - try { - const { image, video } = parseLivePhotoExportName(exportName); - if ( - ls.has(joinPath(collectionExportPath, image)) && - ls.has(joinPath(collectionExportPath, video)) - ) { - result.add(recordID); - } - } catch { - /* Not an error, the file just might not exist on disk yet */ - } - } - } - - return result; -}; - -/** - * Return the list of files from amongst {@link allFiles} that still need to be - * exported. - * - * @param allFiles The list of files to export. - * - * @param exportRecord The export record containing bookkeeping for the export. - * - * @paramd diskFileRecordIDs (Optional) The export record IDs of files from - * amongst {@link allFiles} that already exist on disk. If provided (e.g. when - * doing a resync), we perform an extra check for on-disk existence instead of - * relying solely on the export record. - */ -const getUnExportedFiles = ( - allFiles: EnteFile[], - exportRecord: ExportRecord, - diskFileRecordIDs: Set | undefined, -) => { - if (!exportRecord?.fileExportNames) { - return allFiles; - } - const exportedFiles = new Set(Object.keys(exportRecord?.fileExportNames)); - return allFiles.filter((file) => { - const recordID = getExportRecordFileUID(file); - if (!exportedFiles.has(recordID)) return true; - if (diskFileRecordIDs && !diskFileRecordIDs.has(recordID)) return true; - return false; - }); -}; - -const getDeletedExportedFiles = ( - allFiles: EnteFile[], - exportRecord: ExportRecord, -): string[] => { - if (!exportRecord?.fileExportNames) { - return []; - } - const presentFileUIDs = new Set( - allFiles?.map((file) => getExportRecordFileUID(file)), - ); - const deletedExportedFiles = Object.keys( - exportRecord?.fileExportNames, - ).filter((fileUID) => { - if (!presentFileUIDs.has(fileUID)) { - return true; - } - return false; - }); - return deletedExportedFiles; -}; - -const getCollectionExportedFiles = ( - exportRecord: ExportRecord, - collectionID: number, -): string[] => { - if (!exportRecord?.fileExportNames) { - return []; - } - const collectionExportedFiles = Object.keys( - exportRecord?.fileExportNames, - ).filter((fileUID) => { - const fileCollectionID = Number(fileUID.split("_")[1]); - if (fileCollectionID === collectionID) { - return true; - } else { - return false; - } - }); - return collectionExportedFiles; -}; - -const getGoogleLikeMetadataFile = ( - fileExportName: string, - file: EnteFile, - dateTimeFormatter: Intl.DateTimeFormat, -) => { - const metadata: Metadata = file.metadata; - const publicMagicMetadata = file.pubMagicMetadata?.data; - const creationTime = Math.floor( - (publicMagicMetadata?.editedTime ?? metadata.creationTime) / 1e6, - ); - const modificationTime = Math.floor( - (metadata.modificationTime ?? metadata.creationTime) / 1e6, - ); - const result: Record = { - title: fileExportName, - creationTime: { - timestamp: `${creationTime}`, - formatted: dateTimeFormatter.format(creationTime * 1000), - }, - modificationTime: { - timestamp: `${modificationTime}`, - formatted: dateTimeFormatter.format(modificationTime * 1000), - }, - }; - const caption = file?.pubMagicMetadata?.data?.caption; - if (caption) result.caption = caption; - const geoData = fileLocation(file); - if (geoData) result.geoData = geoData; - return JSON.stringify(result, null, 2); -}; - -export const getMetadataFolderExportPath = (collectionExportPath: string) => - joinPath(collectionExportPath, exportMetadataDirectoryName); - -// if filepath is /home/user/Ente/Export/Collection1/1.jpg -// then metadata path is /home/user/Ente/Export/Collection1/ENTE_METADATA_FOLDER/1.jpg.json -const getFileMetadataExportPath = ( - collectionExportPath: string, - fileExportName: string, -) => - joinPath( - collectionExportPath, - joinPath(exportMetadataDirectoryName, `${fileExportName}.json`), - ); - -export const getLivePhotoExportName = ( - imageExportName: string, - videoExportName: string, -) => JSON.stringify({ image: imageExportName, video: videoExportName }); - -export const isLivePhotoExportName = (exportName: string) => { - try { - JSON.parse(exportName); - return true; - } catch { - return false; - } -}; - -const parseLivePhotoExportName = ( - livePhotoExportName: string, -): { image: string; video: string } => { - const { image, video } = JSON.parse(livePhotoExportName); - return { image, video }; -}; - -const isExportInProgress = (exportStage: ExportStage) => - exportStage > ExportStage.init && exportStage < ExportStage.finished; - -/** - * Move {@link fileName} in {@link collectionName} to the special per-collection - * file system "Trash" folder we created under the export directory. - * - * Also move its associated metadata JSON to Trash. - * - * @param exportDir The root directory on the user's file system where we are - * exporting to. - * */ -const moveToFSTrash = async ( - exportDir: string, - collectionName: string, - fileName: string, -) => { - const fs = ensureElectron().fs; - - const filePath = joinPath(exportDir, joinPath(collectionName, fileName)); - const trashDir = joinPath( - exportDir, - joinPath(exportTrashDirectoryName, collectionName), - ); - const metadataFileName = `${fileName}.json`; - const metadataFilePath = joinPath( - exportDir, - joinPath( - collectionName, - joinPath(exportMetadataDirectoryName, metadataFileName), - ), - ); - const metadataTrashDir = joinPath( - exportDir, - joinPath( - exportTrashDirectoryName, - joinPath(collectionName, exportMetadataDirectoryName), - ), - ); - - log.info(`Moving file ${filePath} and its metadata to trash folder`); - - if (await fs.exists(filePath)) { - await fs.mkdirIfNeeded(trashDir); - const trashFileName = await safeFileName(trashDir, fileName, fs.exists); - const trashFilePath = joinPath(trashDir, trashFileName); - await fs.rename(filePath, trashFilePath); - } - - if (await fs.exists(metadataFilePath)) { - await fs.mkdirIfNeeded(metadataTrashDir); - const metadataTrashFileName = await safeFileName( - metadataTrashDir, - metadataFileName, - fs.exists, - ); - const metadataTrashFilePath = joinPath( - metadataTrashDir, - metadataTrashFileName, - ); - await fs.rename(metadataFilePath, metadataTrashFilePath); - } -}; diff --git a/web/apps/photos/src/services/logout.ts b/web/apps/photos/src/services/logout.ts index c17796a39a..c70f3a8b54 100644 --- a/web/apps/photos/src/services/logout.ts +++ b/web/apps/photos/src/services/logout.ts @@ -7,11 +7,11 @@ import { logoutFileViewerDataSource } from "ente-gallery/components/viewer/data- import { downloadManager } from "ente-gallery/services/download"; import { resetUploadState } from "ente-gallery/services/upload"; import { resetVideoState } from "ente-gallery/services/video"; +import exportService from "ente-new/photos/services/export"; import { logoutML, terminateMLWorker } from "ente-new/photos/services/ml"; import { logoutSearch } from "ente-new/photos/services/search"; import { logoutSettings } from "ente-new/photos/services/settings"; import { logoutUserDetails } from "ente-new/photos/services/user-details"; -import exportService from "./export"; import { uploadManager } from "./upload-manager"; /** diff --git a/web/apps/photos/src/services/upload-manager.ts b/web/apps/photos/src/services/upload-manager.ts index 80fdce4abd..6cce6aaf43 100644 --- a/web/apps/photos/src/services/upload-manager.ts +++ b/web/apps/photos/src/services/upload-manager.ts @@ -1,4 +1,3 @@ -import { Canceler } from "axios"; import { isDesktop } from "ente-base/app"; import { createComlinkCryptoWorker } from "ente-base/crypto"; import { type CryptoWorker } from "ente-base/crypto/worker"; @@ -7,7 +6,6 @@ import type { PublicAlbumsCredentials } from "ente-base/http"; import log from "ente-base/log"; import { ComlinkWorker } from "ente-base/worker/comlink-worker"; import { - RANDOM_PERCENTAGE_PROGRESS_FOR_PUT, markUploadedAndObtainProcessableItem, shouldDisableCFUploadProxy, type ClusteredUploadItem, @@ -19,7 +17,7 @@ import { metadataJSONMapKeyForJSON, tryParseTakeoutMetadataJSON, type ParsedMetadataJSON, -} from "ente-gallery/services/upload/takeout"; +} from "ente-gallery/services/upload/metadata-json"; import UploadService, { areLivePhotoAssets, upload, @@ -64,16 +62,23 @@ export interface InProgressUpload { progress: PercentageUploaded; } +/** + * A variant of {@link UploadResult} used when segregating finished uploads in + * the UI. "addedSymlink" is treated as "uploaded", everything else remains as + * it were. + */ +export type FinishedUploadResult = Exclude; + export interface FinishedUpload { localFileID: FileID; - result: UploadResult; + result: FinishedUploadResult; } export type InProgressUploads = Map; -export type FinishedUploads = Map; +export type FinishedUploads = Map; -export type SegregatedFinishedUploads = Map; +export type SegregatedFinishedUploads = Map; export interface ProgressUpdater { setPercentComplete: React.Dispatch>; @@ -158,7 +163,7 @@ class UIService { this.setTotalFileCount(count); this.filesUploadedCount = 0; this.inProgressUploads = new Map(); - this.finishedUploads = new Map(); + this.finishedUploads = new Map(); this.updateProgressBarUI(); } @@ -202,7 +207,7 @@ class UIService { this.updateProgressBarUI(); } - moveFileToResultList(key: number, uploadResult: UploadResult) { + moveFileToResultList(key: number, uploadResult: FinishedUploadResult) { this.finishedUploads.set(key, uploadResult); this.inProgressUploads.delete(key); this.updateProgressBarUI(); @@ -244,48 +249,16 @@ class UIService { setFinishedUploads(groupByResult(this.finishedUploads)); } - trackUploadProgress( - fileLocalID: number, - percentPerPart = RANDOM_PERCENTAGE_PROGRESS_FOR_PUT(), - index = 0, - ) { - const cancel: { exec: Canceler } = { exec: () => {} }; - const cancelTimedOutRequest = () => cancel.exec("Request timed out"); - - const cancelCancelledUploadRequest = () => - cancel.exec(CustomError.UPLOAD_CANCELLED); - - let timeout = null; - const resetTimeout = () => { - if (timeout) { - clearTimeout(timeout); - } - timeout = setTimeout(cancelTimedOutRequest, 30 * 1000 /* 30 sec */); - }; - return { - cancel, - onUploadProgress: (event) => { - this.inProgressUploads.set( - fileLocalID, - Math.min( - Math.round( - percentPerPart * index + - (percentPerPart * event.loaded) / event.total, - ), - 98, - ), - ); - this.updateProgressBarUI(); - if (event.loaded === event.total) { - clearTimeout(timeout); - } else { - resetTimeout(); - } - if (uploadCancelService.isUploadCancelationRequested()) { - cancelCancelledUploadRequest(); - } - }, - }; + /** + * Update the upload progress shown in the UI to {@link percentage} for the + * file with the given {@link fileLocalID}. + * + * @param percentage The upload completion percentage. It should be a value + * between 0 and 100 (inclusive). + */ + updateUploadProgress(fileLocalID: number, percentage: number) { + this.inProgressUploads.set(fileLocalID, Math.round(percentage)); + this.updateProgressBarUI(); } } @@ -352,10 +325,17 @@ class UploadManager { this.uploaderName = null; } - public prepareForNewUpload() { + public prepareForNewUpload( + parsedMetadataJSONMap?: Map, + ) { this.resetState(); this.uiService.reset(); uploadCancelService.reset(); + + if (parsedMetadataJSONMap) { + this.parsedMetadataJSONMap = parsedMetadataJSONMap; + } + this.uiService.setUploadPhase("preparing"); } @@ -474,6 +454,7 @@ class UploadManager { const item = { uploadItem: file, + pathPrefix: undefined, localID: 1, collectionID: collection.id, externalParsedMetadata: { creationDate, creationTime }, @@ -508,16 +489,19 @@ class UploadManager { ) { this.uiService.reset(items.length); - for (const { uploadItem, fileName, collectionID } of items) { + for (const item of items) { this.abortIfCancelled(); + const { uploadItem, pathPrefix, fileName, collectionID } = item; log.info(`Parsing metadata JSON ${fileName}`); const metadataJSON = await tryParseTakeoutMetadataJSON(uploadItem!); if (metadataJSON) { - this.parsedMetadataJSONMap.set( - metadataJSONMapKeyForJSON(collectionID, fileName), - metadataJSON, + const key = metadataJSONMapKeyForJSON( + pathPrefix, + collectionID, + fileName, ); + this.parsedMetadataJSONMap.set(key, metadataJSON); this.uiService.increaseFileUploaded(); } } @@ -544,6 +528,12 @@ class UploadManager { private async uploadNextItemInQueue(worker: CryptoWorker) { const uiService = this.uiService; + const uploadContext = { + isCFUploadProxyDisabled: shouldDisableCFUploadProxy(), + abortIfCancelled: this.abortIfCancelled.bind(this), + updateUploadProgress: + uiService.updateUploadProgress.bind(uiService), + }; while (this.itemsToBeUploaded.length > 0) { this.abortIfCancelled(); @@ -563,20 +553,7 @@ class UploadManager { this.existingFiles, this.parsedMetadataJSONMap, worker, - shouldDisableCFUploadProxy(), - () => { - this.abortIfCancelled(); - }, - ( - fileLocalID: number, - percentPerPart?: number, - index?: number, - ) => - uiService.trackUploadProgress( - fileLocalID, - percentPerPart, - index, - ), + uploadContext, ); const finalUploadResult = await this.postUploadTask( @@ -585,8 +562,8 @@ class UploadManager { uploadedFile, ); - this.uiService.moveFileToResultList(localID, finalUploadResult); - this.uiService.increaseFileUploaded(); + uiService.moveFileToResultList(localID, finalUploadResult); + uiService.increaseFileUploaded(); UploadService.reducePendingUploadCount(); } } @@ -595,8 +572,10 @@ class UploadManager { uploadableItem: UploadableUploadItem, uploadResult: UploadResult, uploadedFile: EncryptedEnteFile | EnteFile | undefined, - ) { + ): Promise { log.info(`Upload ${uploadableItem.fileName} | ${uploadResult}`); + const finishedUploadResult = + uploadResult == "addedSymlink" ? "uploaded" : uploadResult; try { const processableUploadItem = await markUploadedAndObtainProcessableItem(uploadableItem); @@ -608,11 +587,8 @@ class UploadManager { this.failedItems.push(uploadableItem); break; case "alreadyUploaded": - decryptedFile = uploadedFile as EnteFile; - break; case "addedSymlink": decryptedFile = uploadedFile as EnteFile; - uploadResult = "uploaded"; break; case "uploaded": case "uploadedWithStaticThumbnail": @@ -653,7 +629,7 @@ class UploadManager { uploadableItem, uploadedFile as EncryptedEnteFile, ); - return uploadResult; + return finishedUploadResult; } catch (e) { log.error("Post file upload action failed", e); return "failed"; @@ -682,10 +658,15 @@ class UploadManager { uploadCancelService.requestUploadCancelation(); } - public getFailedItemsWithCollections() { + /** + * Return the list of failed items from the last upload, along with other + * state needed to attempt to reupload them. + */ + public failedItemState() { return { - items: this.failedItems, + items: [...this.failedItems], collections: [...this.collections.values()], + parsedMetadataJSONMap: this.parsedMetadataJSONMap, }; } @@ -701,8 +682,13 @@ class UploadManager { this.onUploadFile(decryptedFile); } - public shouldAllowNewUpload = () => { - return !this.uploadInProgress || watcher.isUploadRunning(); + /** + * `true` if an upload is currently in-progress (either a bunch of files + * directly uploaded by the user, or files being uploaded by the folder + * watch functionality). + */ + public isUploadInProgress = () => { + return this.uploadInProgress || watcher.isUploadRunning(); }; } @@ -760,6 +746,7 @@ const makeUploadItemWithCollectionIDAndName = ( : uploadItemFileName(f.uploadItem))!, isLivePhoto: f.isLivePhoto, uploadItem: f.uploadItem, + pathPrefix: f.pathPrefix, livePhotoAssets: f.livePhotoAssets, externalParsedMetadata: f.externalParsedMetadata, }); @@ -806,12 +793,14 @@ const clusterLivePhotos = async ( fileType: fFileType, collectionID: f.collectionID, uploadItem: f.uploadItem, + pathPrefix: f.pathPrefix, }; const ga: PotentialLivePhotoAsset = { fileName: g.fileName, fileType: gFileType, collectionID: g.collectionID, uploadItem: g.uploadItem, + pathPrefix: g.pathPrefix, }; if (await areLivePhotoAssets(fa, ga, parsedMetadataJSONMap)) { const [image, video] = @@ -821,6 +810,7 @@ const clusterLivePhotos = async ( collectionID: f.collectionID, fileName: image.fileName, isLivePhoto: true, + pathPrefix: image.pathPrefix, livePhotoAssets: { image: image.uploadItem, video: video.uploadItem, @@ -828,12 +818,15 @@ const clusterLivePhotos = async ( }); index += 2; } else { - result.push({ ...f, isLivePhoto: false }); + // They may already be a live photo (we might be retrying a + // previously failed upload). + result.push({ ...f, isLivePhoto: f.isLivePhoto ?? false }); index += 1; } } - if (index === items.length - 1) { - result.push({ ...items[index], isLivePhoto: false }); + if (index == items.length - 1) { + const f = items[index]!; + result.push({ ...f, isLivePhoto: f.isLivePhoto ?? false }); } return result; }; diff --git a/web/apps/photos/src/services/userService.ts b/web/apps/photos/src/services/userService.ts deleted file mode 100644 index 2665996f1d..0000000000 --- a/web/apps/photos/src/services/userService.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { HttpStatusCode } from "axios"; -import { putAttributes } from "ente-accounts/services/user"; -import log from "ente-base/log"; -import { apiURL } from "ente-base/origins"; -import type { UserDetails } from "ente-new/photos/services/user-details"; -import { ApiError } from "ente-shared/error"; -import HTTPService from "ente-shared/network/HTTPService"; -import { getData } from "ente-shared/storage/localStorage"; -import { getToken } from "ente-shared/storage/localStorage/helpers"; - -const HAS_SET_KEYS = "hasSetKeys"; - -export const getPublicKey = async (email: string) => { - const token = getToken(); - - const resp = await HTTPService.get( - await apiURL("/users/public-key"), - { email }, - { "X-Auth-Token": token }, - ); - return resp.data.publicKey; -}; - -export const isTokenValid = async (token: string) => { - try { - const resp = await HTTPService.get( - await apiURL("/users/session-validity/v2"), - null, - { "X-Auth-Token": token }, - ); - try { - if (resp.data[HAS_SET_KEYS] === undefined) { - throw Error("resp.data.hasSetKey undefined"); - } - if (!resp.data.hasSetKeys) { - try { - await putAttributes( - token, - getData("originalKeyAttributes"), - ); - } catch (e) { - log.error("put attribute failed", e); - } - } - } catch (e) { - log.error("hasSetKeys not set in session validity response", e); - } - return true; - } catch (e) { - log.error("session-validity api call failed", e); - if ( - e instanceof ApiError && - e.httpStatusCode === HttpStatusCode.Unauthorized - ) { - return false; - } else { - return true; - } - } -}; - -export const getUserDetailsV2 = async (): Promise => { - try { - const token = getToken(); - - const resp = await HTTPService.get( - await apiURL("/users/details/v2"), - null, - { "X-Auth-Token": token }, - ); - return resp.data; - } catch (e) { - log.error("failed to get user details v2", e); - throw e; - } -}; diff --git a/web/apps/photos/src/utils/collection.ts b/web/apps/photos/src/utils/collection.ts index 5e1b46681c..7ce407387d 100644 --- a/web/apps/photos/src/utils/collection.ts +++ b/web/apps/photos/src/utils/collection.ts @@ -31,7 +31,6 @@ import { import { safeDirectoryName } from "ente-new/photos/utils/native-fs"; import { getData } from "ente-shared/storage/localStorage"; import type { User } from "ente-shared/user/types"; -import { t } from "i18next"; import { createAlbum, removeFromCollection, @@ -183,15 +182,6 @@ async function createCollectionDownloadFolder( return collectionDownloadPath; } -const _intSelectOption = (i: number) => { - const label = i === 0 ? t("none") : i.toString(); - return { label, value: i }; -}; - -export function getDeviceLimitOptions() { - return [0, 2, 5, 10, 25, 50].map((i) => _intSelectOption(i)); -} - export const changeCollectionVisibility = async ( collection: Collection, visibility: ItemVisibility, diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index 005c815986..9b5d90257f 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -22,6 +22,7 @@ import { import { safeFileName } from "ente-new/photos/utils/native-fs"; import { getData } from "ente-shared/storage/localStorage"; import type { User } from "ente-shared/user/types"; +import { wait } from "ente-utils/promise"; import { t } from "i18next"; import { addMultipleToFavorites, @@ -60,6 +61,9 @@ export async function downloadFile(file: EnteFile) { new Blob([videoData], { type: videoType.mimeType }), ); downloadAndRevokeObjectURL(tempImageURL, imageFileName); + // Downloading multiple works everywhere except, you guessed it, + // Safari. Make up for their incompetence by adding a setTimeout. + await wait(300) /* arbitrary constant, 300ms */; downloadAndRevokeObjectURL(tempVideoURL, videoFileName); } else { const fileType = await detectFileTypeInfo( @@ -338,26 +342,6 @@ export const getUserOwnedFiles = (files: EnteFile[]) => { return files.filter((file) => file.ownerID === user.id); }; -export function getPersonalFiles( - files: EnteFile[], - user: User, - collectionIdToOwnerIDMap?: Map, -) { - if (!user?.id) { - throw Error("user missing"); - } - return files.filter( - (file) => - file.ownerID === user.id && - (!collectionIdToOwnerIDMap || - collectionIdToOwnerIDMap.get(file.collectionID) === user.id), - ); -} - -export function getIDBasedSortedFiles(files: EnteFile[]) { - return files.sort((a, b) => a.id - b.id); -} - export const shouldShowAvatar = (file: EnteFile, user: User) => { if (!file || !user) { return false; diff --git a/web/apps/photos/tests/upload.test.ts b/web/apps/photos/tests/upload.test.ts index b918f52cc9..ceb31acbc7 100644 --- a/web/apps/photos/tests/upload.test.ts +++ b/web/apps/photos/tests/upload.test.ts @@ -4,16 +4,16 @@ import { tryParseEpochMicrosecondsFromFileName, } from "ente-gallery/services/upload/date"; import { - matchTakeoutMetadata, + matchJSONMetadata, metadataJSONMapKeyForJSON, -} from "ente-gallery/services/upload/takeout"; +} from "ente-gallery/services/upload/metadata-json"; import { FileType } from "ente-media/file-type"; import { getLocalCollections } from "ente-new/photos/services/collections"; import { getLocalFiles, groupFilesByCollectionID, } from "ente-new/photos/services/files"; -import { getUserDetailsV2 } from "services/userService"; +import { getUserDetailsV2 } from "ente-new/photos/services/user-details"; const DATE_TIME_PARSING_TEST_FILE_NAMES = [ { @@ -447,14 +447,14 @@ function parseDateTimeFromFileNameTest() { const fileNameToJSONMappingTests = () => { for (const { filename, jsonFilename } of fileNameToJSONMappingCases) { - const jsonKey = metadataJSONMapKeyForJSON(0, jsonFilename); + const jsonKey = metadataJSONMapKeyForJSON(undefined, 0, jsonFilename); // See the docs for the file name matcher as to why it doesn't return // the key but instead indexes into the map for us. To test it, we // construct a placeholder map with a dummy entry for the expected key. const map = new Map([[jsonKey, {}]]); - if (!matchTakeoutMetadata(filename, 0, map)) { + if (!matchJSONMetadata(undefined, 0, filename, map)) { throw Error( `fileNameToJSONMappingTests failed ❌ for ${filename} and ${jsonFilename}`, ); diff --git a/web/package.json b/web/package.json index 05b9b0410e..c0d8f14bc9 100644 --- a/web/package.json +++ b/web/package.json @@ -29,7 +29,7 @@ }, "devDependencies": { "concurrently": "^9.1.2", - "eslint": "^9.25.1", + "eslint": "^9.27.0", "prettier": "^3.5.3", "typescript": "^5.8.3" }, diff --git a/web/packages/accounts/components/SetPasswordForm.tsx b/web/packages/accounts/components/SetPasswordForm.tsx index 4c407b8137..90c65b951a 100644 --- a/web/packages/accounts/components/SetPasswordForm.tsx +++ b/web/packages/accounts/components/SetPasswordForm.tsx @@ -1,10 +1,10 @@ import { Box, Input, TextField, Typography } from "@mui/material"; import { isWeakPassword } from "ente-accounts/utils/password"; import { LoadingButton } from "ente-base/components/mui/LoadingButton"; -import ShowHidePassword from "ente-shared/components/Form/ShowHidePassword"; +import { ShowHidePasswordInputAdornment } from "ente-base/components/mui/PasswordInputAdornment"; import { Formik } from "formik"; import { t } from "i18next"; -import React, { useState } from "react"; +import { useCallback, useState } from "react"; import { Trans } from "react-i18next"; import * as Yup from "yup"; import { PasswordStrengthHint } from "./PasswordStrength"; @@ -30,15 +30,10 @@ function SetPasswordForm(props: SetPasswordFormProps) { const [loading, setLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); - const handleClickShowPassword = () => { - setShowPassword(!showPassword); - }; - - const handleMouseDownPassword = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - }; + const handleToggleShowHidePassword = useCallback( + () => setShowPassword((show) => !show), + [], + ); const onSubmit = async ( values: SetPasswordFormValues, @@ -111,14 +106,9 @@ function SetPasswordForm(props: SetPasswordFormProps) { slotProps={{ input: { endAdornment: ( - ), }, diff --git a/web/packages/accounts/components/SignUpContents.tsx b/web/packages/accounts/components/SignUpContents.tsx index c06777668b..c9611ee3d4 100644 --- a/web/packages/accounts/components/SignUpContents.tsx +++ b/web/packages/accounts/components/SignUpContents.tsx @@ -18,11 +18,10 @@ import { sendOTT } from "ente-accounts/services/user"; import { isWeakPassword } from "ente-accounts/utils/password"; import { LinkButton } from "ente-base/components/LinkButton"; import { LoadingButton } from "ente-base/components/mui/LoadingButton"; +import { ShowHidePasswordInputAdornment } from "ente-base/components/mui/PasswordInputAdornment"; import { isMuseumHTTPError } from "ente-base/http"; import log from "ente-base/log"; import { setLSUser } from "ente-shared//storage/localStorage"; -import { VerticallyCentered } from "ente-shared/components/Container"; -import ShowHidePassword from "ente-shared/components/Form/ShowHidePassword"; import { generateAndSaveIntermediateKeyAttributes, saveKeyInSessionStore, @@ -35,7 +34,7 @@ import { import { Formik, type FormikHelpers } from "formik"; import { t } from "i18next"; import type { NextRouter } from "next/router"; -import React, { useState } from "react"; +import React, { useCallback, useState } from "react"; import { Trans } from "react-i18next"; import * as Yup from "yup"; import { PasswordStrengthHint } from "./PasswordStrength"; @@ -68,15 +67,10 @@ export const SignUpContents: React.FC = ({ const [loading, setLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); - const handleClickShowPassword = () => { - setShowPassword(!showPassword); - }; - - const handleMouseDownPassword = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - }; + const handleToggleShowHidePassword = useCallback( + () => setShowPassword((show) => !show), + [], + ); const registerUser = async ( { email, passphrase, confirm, referral }: FormValues, @@ -154,7 +148,7 @@ export const SignUpContents: React.FC = ({ handleSubmit, }): React.JSX.Element => (
- + = ({ slotProps={{ input: { endAdornment: ( - ), @@ -298,7 +289,7 @@ export const SignUpContents: React.FC = ({ } /> - + { <> {showMessage && ( } + severity="success" onClose={() => setShowMessage(false)} > { )} - + { > {!ottInputVisible ? t("send_otp") : t("verify")} - + diff --git a/web/packages/accounts/pages/generate.tsx b/web/packages/accounts/pages/generate.tsx index 1c7211b9cd..0f72c562c0 100644 --- a/web/packages/accounts/pages/generate.tsx +++ b/web/packages/accounts/pages/generate.tsx @@ -12,7 +12,7 @@ import { configureSRP, generateKeyAndSRPAttributes, } from "ente-accounts/services/srp"; -import { putAttributes } from "ente-accounts/services/user"; +import { putUserKeyAttributes } from "ente-accounts/services/user"; import { LinkButton } from "ente-base/components/LinkButton"; import { LoadingIndicator } from "ente-base/components/loaders"; import { useBaseContext } from "ente-base/context"; @@ -35,7 +35,6 @@ import { useEffect, useState } from "react"; const Page: React.FC = () => { const { logout, showMiniDialog } = useBaseContext(); - const [token, setToken] = useState(); const [user, setUser] = useState(); const [openRecoveryKey, setOpenRecoveryKey] = useState(false); const [loading, setLoading] = useState(true); @@ -59,7 +58,6 @@ const Page: React.FC = () => { } else if (keyAttributes?.encryptedKey) { void router.push("/credentials"); } else { - setToken(user.token); setLoading(false); } }, [router]); @@ -72,8 +70,7 @@ const Page: React.FC = () => { const { keyAttributes, masterKey, srpSetupAttributes } = await generateKeyAndSRPAttributes(passphrase); - // TODO: Refactor the code to not require this ensure - await putAttributes(token!, keyAttributes); + await putUserKeyAttributes(keyAttributes); await configureSRP(srpSetupAttributes); await generateAndSaveIntermediateKeyAttributes( passphrase, diff --git a/web/packages/accounts/pages/verify.tsx b/web/packages/accounts/pages/verify.tsx index 395278ab16..39e33756c1 100644 --- a/web/packages/accounts/pages/verify.tsx +++ b/web/packages/accounts/pages/verify.tsx @@ -23,17 +23,20 @@ import type { } from "ente-accounts/services/srp-remote"; import { getSRPAttributes } from "ente-accounts/services/srp-remote"; import { - putAttributes, + putUserKeyAttributes, sendOTT, verifyEmail, } from "ente-accounts/services/user"; import { LinkButton } from "ente-base/components/LinkButton"; import { LoadingIndicator } from "ente-base/components/loaders"; -import { useBaseContext } from "ente-base/context"; -import log from "ente-base/log"; -import SingleInputForm, { +import { + SingleInputForm, type SingleInputFormProps, -} from "ente-shared/components/SingleInputForm"; +} from "ente-base/components/SingleInputForm"; +import { useBaseContext } from "ente-base/context"; +import { isHTTPErrorWithStatus } from "ente-base/http"; +import log from "ente-base/log"; +import { clearSessionStorage } from "ente-base/session"; import { ApiError } from "ente-shared/error"; import localForage from "ente-shared/storage/localForage"; import { getData, setData, setLSUser } from "ente-shared/storage/localStorage"; @@ -41,7 +44,6 @@ import { getLocalReferralSource, setIsFirstLogin, } from "ente-shared/storage/localStorage/helpers"; -import { clearKeys } from "ente-shared/storage/sessionStorage"; import type { KeyAttributes, User } from "ente-shared/user/types"; import { t } from "i18next"; import { useRouter } from "next/router"; @@ -77,7 +79,7 @@ const Page: React.FC = () => { void main(); }, [router]); - const onSubmit: SingleInputFormProps["callback"] = async ( + const onSubmit: SingleInputFormProps["onSubmit"] = async ( ott, setFieldError, ) => { @@ -137,11 +139,11 @@ const Page: React.FC = () => { setData("keyAttributes", keyAttributes); setData("originalKeyAttributes", keyAttributes); } else { - if (getData("originalKeyAttributes")) { - await putAttributes( - token!, - getData("originalKeyAttributes"), - ); + const originalKeyAttributes = getData( + "originalKeyAttributes", + ); + if (originalKeyAttributes) { + await putUserKeyAttributes(originalKeyAttributes); } if (getData("srpSetupAttributes")) { const srpSetupAttributes: SRPSetupAttributes = @@ -153,14 +155,18 @@ const Page: React.FC = () => { setIsFirstLogin(true); const redirectURL = unstashRedirect(); if (keyAttributes?.encryptedKey) { - clearKeys(); + clearSessionStorage(); void router.push(redirectURL ?? "/credentials"); } else { void router.push(redirectURL ?? "/generate"); } } } catch (e) { - if (e instanceof ApiError) { + if (isHTTPErrorWithStatus(e, 401)) { + setFieldError(t("invalid_code_error")); + } else if (isHTTPErrorWithStatus(e, 410)) { + setFieldError(t("expired_code_error")); + } else if (e instanceof ApiError) { // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison if (e?.httpStatusCode === HttpStatusCode.Unauthorized) { setFieldError(t("invalid_code_error")); @@ -170,7 +176,7 @@ const Page: React.FC = () => { } } else { log.error("OTT verification failed", e); - setFieldError(t("generic_error_retry")); + throw e; } } }; @@ -236,11 +242,11 @@ const Page: React.FC = () => { {t("check_inbox_hint")} diff --git a/web/packages/accounts/services/logout.ts b/web/packages/accounts/services/logout.ts index 8d0446189c..5ea2c41fa3 100644 --- a/web/packages/accounts/services/logout.ts +++ b/web/packages/accounts/services/logout.ts @@ -2,8 +2,8 @@ import { clearBlobCaches } from "ente-base/blob-cache"; import { clearKVDB } from "ente-base/kv"; import { clearLocalStorage } from "ente-base/local-storage"; import log from "ente-base/log"; +import { clearSessionStorage } from "ente-base/session"; import localForage from "ente-shared/storage/localForage"; -import { clearKeys } from "ente-shared/storage/sessionStorage"; import { clearStashedRedirect } from "./redirect"; import { remoteLogoutIfNeeded } from "./user"; @@ -34,7 +34,7 @@ export const accountLogout = async () => { ignoreError("In-memory store", e); } try { - clearKeys(); + clearSessionStorage(); } catch (e) { ignoreError("Session storage", e); } diff --git a/web/packages/accounts/services/session.ts b/web/packages/accounts/services/session.ts index a64b181153..b2a4fcae69 100644 --- a/web/packages/accounts/services/session.ts +++ b/web/packages/accounts/services/session.ts @@ -1,10 +1,14 @@ import { authenticatedRequestHeaders, HTTPError } from "ente-base/http"; -import { ensureLocalUser } from "ente-base/local-user"; +import { ensureLocalUser, getAuthToken } from "ente-base/local-user"; +import log from "ente-base/log"; import { apiURL } from "ente-base/origins"; import { getData } from "ente-shared/storage/localStorage"; import type { KeyAttributes } from "ente-shared/user/types"; +import { nullToUndefined } from "ente-utils/transform"; +import { z } from "zod"; import type { SRPAttributes } from "./srp-remote"; import { getSRPAttributes } from "./srp-remote"; +import { putUserKeyAttributes, RemoteKeyAttributes } from "./user"; type SessionValidity = | { status: "invalid" } @@ -15,6 +19,11 @@ type SessionValidity = updatedSRPAttributes: SRPAttributes; }; +const SessionValidityResponse = z.object({ + hasSetKeys: z.boolean(), + keyAttributes: RemoteKeyAttributes.nullish().transform(nullToUndefined), +}); + /** * Check if the local token and/or key attributes we have are still valid. * @@ -72,16 +81,9 @@ export const checkSessionValidity = async (): Promise => { // See if the response contains keyAttributes (they might not for older // deployments). - const json = await res.json(); - if ( - "keyAttributes" in json && - typeof json.keyAttributes == "object" && - json.keyAttributes !== null - ) { - // Assume it is a `KeyAttributes`. - // - // Enhancement: Convert this to a zod validation. - const remoteKeyAttributes = json.keyAttributes as KeyAttributes; + const { keyAttributes } = SessionValidityResponse.parse(await res.json()); + if (keyAttributes) { + const remoteKeyAttributes = keyAttributes; // We should have these values locally if we reach here. const email = ensureLocalUser().email; @@ -106,7 +108,10 @@ export const checkSessionValidity = async (): Promise => { // changed. return { status: "validButPasswordChanged", - updatedKeyAttributes: remoteKeyAttributes, + // TODO: + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + updatedKeyAttributes: remoteKeyAttributes as KeyAttributes, updatedSRPAttributes: remoteSRPAttributes, }; } @@ -116,3 +121,51 @@ export const checkSessionValidity = async (): Promise => { // The token is still valid (to the best of our ascertainable knowledge). return { status: "valid" }; }; + +/** + * Return `true` if the user does not have a saved auth token, of it is no + * longer valid. If needed, also update the key attributes at remote. + * + * This is a subset of {@link checkSessionValidity} that has been tailored for + * use during each remote sync, to detect if the user has been logged out + * elsewhere. + * + * @returns `true` if either we don't have an auth token, or if remote tells us + * that the auth token (and the associated session) has been invalidated. In all + * other cases, return `false`. + * + * In particular, this function doesn't throw and instead returns `false` on + * errors. This is because returning `true` will trigger a blocking alert that + * ends in logging the user out, and we don't want to log the user out on on + * e.g. transient network issues. + */ +export const isSessionInvalid = async (): Promise => { + const token = await getAuthToken(); + if (!token) { + return true; /* No saved token, session is invalid */ + } + + try { + const res = await fetch(await apiURL("/users/session-validity/v2"), { + headers: await authenticatedRequestHeaders(), + }); + if (!res.ok) { + if (res.status == 401) return true; /* session is no longer valid */ + else throw new HTTPError(res); + } + + const { hasSetKeys } = SessionValidityResponse.parse(await res.json()); + if (!hasSetKeys) { + const originalKeyAttributes = getData("originalKeyAttributes"); + if (originalKeyAttributes) + await putUserKeyAttributes(originalKeyAttributes); + } + } catch (e) { + log.warn("Failed to check session validity", e); + // Don't logout user on potentially transient errors. + return false; + } + + // Everything seems ok. + return false; +}; diff --git a/web/packages/accounts/services/user.ts b/web/packages/accounts/services/user.ts index e37e66a189..6f5d55f85e 100644 --- a/web/packages/accounts/services/user.ts +++ b/web/packages/accounts/services/user.ts @@ -127,7 +127,7 @@ export const verifyEmail = async ( /** * Zod schema for {@link KeyAttributes}. */ -const RemoteKeyAttributes = z.object({ +export const RemoteKeyAttributes = z.object({ kekSalt: z.string(), encryptedKey: z.string(), keyDecryptionNonce: z.string(), @@ -195,17 +195,17 @@ export type TwoFactorAuthorizationResponse = z.infer< typeof TwoFactorAuthorizationResponse >; -export const putAttributes = async ( - token: string, - keyAttributes: KeyAttributes, -) => - HTTPService.put( - await apiURL("/users/attributes"), - { keyAttributes }, - undefined, - { "X-Auth-Token": token }, +/** + * Update or set the user's {@link KeyAttributes} on remote. + */ +export const putUserKeyAttributes = async (keyAttributes: KeyAttributes) => + ensureOk( + await fetch(await apiURL("/users/attributes"), { + method: "PUT", + headers: await authenticatedRequestHeaders(), + body: JSON.stringify({ keyAttributes }), + }), ); - /** * Log the user out on remote, if possible and needed. */ diff --git a/web/packages/base/app.ts b/web/packages/base/app.ts index 74b891a9c0..e330e41a83 100644 --- a/web/packages/base/app.ts +++ b/web/packages/base/app.ts @@ -62,8 +62,7 @@ export const staticAppTitle = { /** * Client "package names" for our app. * - * The package name is used as the (a) "X-Client-Package" header in API - * requests, and (b) as the identifier in inline user agent strings in payloads. + * The package name is used as the "X-Client-Package" header in API requests. * * In cases where this code works for both a web and a desktop app for the same * app (currently only photos), this will be the platform specific package name. @@ -72,9 +71,7 @@ export const clientPackageName = (() => { if (isDesktop) { if (appName != "photos") throw new Error(`Unsupported desktop appName ${appName}`); - if (!desktopAppVersion) - throw new Error(`desktopAppVersion is not defined`); - return `io.ente.photos.desktop/${desktopAppVersion}`; + return "io.ente.photos.desktop"; } return { accounts: "io.ente.accounts.web", @@ -83,3 +80,23 @@ export const clientPackageName = (() => { photos: "io.ente.photos.web", }[appName]; })(); + +/** + * The client package name, augmented with the client version where applicable. + * + * This string is used as the "client identifier" in payloads (e.g. ML data + * generated by a client). + * + * For the desktop app, it will be client package name and version. Otherwise, + * it'll just be the same as {@link clientPackageName}. + */ +export const clientIdentifier = (() => { + if (isDesktop) { + if (appName != "photos") + throw new Error(`Unsupported desktop appName ${appName}`); + if (!desktopAppVersion) + throw new Error(`desktopAppVersion is not defined`); + return `io.ente.photos.desktop/${desktopAppVersion}`; + } + return clientPackageName; +})(); diff --git a/web/packages/base/components/SingleInputDialog.tsx b/web/packages/base/components/SingleInputDialog.tsx index 11f23575ad..9a1cf64b69 100644 --- a/web/packages/base/components/SingleInputDialog.tsx +++ b/web/packages/base/components/SingleInputDialog.tsx @@ -1,6 +1,6 @@ import { Dialog, DialogContent, DialogTitle } from "@mui/material"; import type { ModalVisibilityProps } from "ente-base/components/utils/modal"; -import React from "react"; +import React, { useCallback } from "react"; import { SingleInputForm, type SingleInputFormProps } from "./SingleInputForm"; type SingleInputDialogProps = ModalVisibilityProps & @@ -25,10 +25,13 @@ export const SingleInputDialog: React.FC = ({ title, ...rest }) => { - const handleSubmit = async (value: string) => { - await onSubmit(value); - onClose(); - }; + const handleSubmit: SingleInputFormProps["onSubmit"] = useCallback( + async (value, setFieldError) => { + await onSubmit(value, setFieldError); + onClose(); + }, + [onClose, onSubmit], + ); return ( & { + /** + * The type attribute of the HTML input element that will be used. + * + * Default is "text". + * + * In addition to changing the behaviour of the HTML input element, the + * {@link SingleInputForm} component also has special casing for type + * "password", wherein it'll show an adornment at the end of the text field + * allowing the user to show or hide the password. + */ + inputType?: TextFieldProps["type"]; /** * The initial value, if any, to prefill in the input. */ @@ -18,13 +35,22 @@ export type SingleInputFormProps = Pick< * Title for the submit button. */ submitButtonTitle: string; + /** + * Color of the submit button. + * + * Default: "primary". + */ + submitButtonColor?: ButtonProps["color"]; /** * Cancellation handler. * * This function is called when the user activates the cancel button in the * form. + * + * If this is not provided, then only a full width submit button will be + * shown in the form (below the input). */ - onCancel: () => void; + onCancel?: () => void; /** * Submission handler. A callback invoked when the submit button is pressed. * @@ -34,13 +60,29 @@ export type SingleInputFormProps = Pick< * If this function rejects then a generic error helper text is shown below * the text input, and the input (/ buttons) reenabled. * + * This function is also passed an function that can be used to explicitly + * set the error message that as shown below the text input when a specific + * problem occurs during submission. + * * @param name The current value of the text input. + * + * @param setFieldError A function that can be called to set the error message + * shown below the text input if submission fails. + * + * Note that if {@link setFieldError} is called, then the {@link onSubmit} + * function should not throw, otherwise the error message shown by + * {@link setFieldError} will get overwritten by the generic error message. */ - onSubmit: ((name: string) => void) | ((name: string) => Promise); + onSubmit: + | ((name: string, setFieldError: (message: string) => void) => void) + | (( + name: string, + setFieldError: (message: string) => void, + ) => Promise); }; /** - * A TextField and two buttons. + * A TextField and cancel/submit buttons. * * A common requirement is taking a single textual input from the user. This is * a form suitable for that purpose. It contains a single MUI {@link TextField} @@ -51,29 +93,52 @@ export type SingleInputFormProps = Pick< * as the helper text associated with the text field. */ export const SingleInputForm: React.FC = ({ + inputType, initialValue, submitButtonTitle, + submitButtonColor, onCancel, onSubmit, ...rest }) => { + const [showPassword, setShowPassword] = useState(false); + + const handleToggleShowHidePassword = useCallback( + () => setShowPassword((show) => !show), + [], + ); + const formik = useFormik({ initialValues: { value: initialValue ?? "" }, onSubmit: async (values, { setFieldError }) => { const value = values.value; + const setValueFieldError = (message: string) => + setFieldError("value", message); + if (!value) { - setFieldError("value", t("required")); + setValueFieldError(t("required")); return; } try { - await onSubmit(value); + await onSubmit(value, setValueFieldError); } catch (e) { log.error(`Failed to submit input ${value}`, e); - setFieldError("value", t("generic_error")); + setValueFieldError(t("generic_error")); } }, }); + const submitButton = ( + + {submitButtonTitle} + + ); + // Note: [Use space as default TextField helperText] // // For MUI text fields that use a conditional helperText, e.g. in case of @@ -86,31 +151,43 @@ export const SingleInputForm: React.FC = ({ name="value" value={formik.values.value} onChange={formik.handleChange} - type="text" + type={showPassword ? "text" : (inputType ?? "text")} fullWidth margin="normal" disabled={formik.isSubmitting} error={!!formik.errors.value} helperText={formik.errors.value ?? " "} + slotProps={{ + input: + inputType == "password" + ? { + endAdornment: ( + + ), + } + : {}, + }} {...rest} /> - - - {t("cancel")} - - - {submitButtonTitle} - - + {onCancel ? ( + + + {t("cancel")} + + {submitButton} + + ) : ( + submitButton + )} ); }; diff --git a/web/packages/base/components/Titlebar.tsx b/web/packages/base/components/Titlebar.tsx deleted file mode 100644 index 9a1f7cc6ed..0000000000 --- a/web/packages/base/components/Titlebar.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import CloseIcon from "@mui/icons-material/Close"; -import { Box, IconButton, Stack, Typography } from "@mui/material"; -import { FlexWrapper } from "ente-shared/components/Container"; -import React from "react"; - -interface TitlebarProps { - title: string; - caption?: string; - onClose: () => void; - backIsClose?: boolean; - onRootClose?: () => void; - actionButton?: React.JSX.Element; -} - -// TODO: Deprecated in favor of SidebarDrawerTitlebarProps where possible (will -// revisit the remaining use cases once those have migrated). -export const Titlebar: React.FC = ({ - title, - caption, - onClose, - backIsClose, - actionButton, - onRootClose, -}) => { - return ( - <> - - - {backIsClose ? : } - - - {actionButton && actionButton} - {!backIsClose && ( - - - - )} - - - - {title} - - {caption} - - - - ); -}; diff --git a/web/packages/base/components/mui/PasswordInputAdornment.tsx b/web/packages/base/components/mui/PasswordInputAdornment.tsx new file mode 100644 index 0000000000..08c4771bc6 --- /dev/null +++ b/web/packages/base/components/mui/PasswordInputAdornment.tsx @@ -0,0 +1,49 @@ +import VisibilityIcon from "@mui/icons-material/Visibility"; +import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; +import { IconButton, InputAdornment } from "@mui/material"; +import { t } from "i18next"; +import React from "react"; + +interface ShowHidePasswordInputAdornmentProps { + /** + * When `true`, the password is being shown. + */ + showPassword: boolean; + /** + * Called when the user wants to toggle the state of {@link showPassword}. + */ + onToggle: () => void; +} + +/** + * A MUI {@link InputAdornment} that can be used at the trailing edge of a + * password input field to allow the user to toggle the visibility of the + * password. + */ +export const ShowHidePasswordInputAdornment: React.FC< + ShowHidePasswordInputAdornmentProps +> = ({ showPassword, onToggle: onToggle }) => { + // Prevent password field from losing focus when the input adornment is + // clicked by ignoring both the mouse up and down events. This is the + // approach mentioned in the MUI docs. + // https://mui.com/material-ui/react-text-field/#input-adornments + const preventDefault = (event: React.MouseEvent) => { + event.preventDefault(); + }; + + return ( + + + {showPassword ? : } + + + ); +}; diff --git a/web/packages/base/components/mui/SidebarDrawer.tsx b/web/packages/base/components/mui/SidebarDrawer.tsx index 73b86d9869..93862059e6 100644 --- a/web/packages/base/components/mui/SidebarDrawer.tsx +++ b/web/packages/base/components/mui/SidebarDrawer.tsx @@ -164,18 +164,43 @@ export const SidebarDrawerTitlebar: React.FC = ({
- + {title} {caption} - + ); + +/** + * A variant of {@link NestedSidebarDrawer} that additionally shows a title. + * + * {@link NestedSidebarDrawer} is for second level, nested drawers that are + * shown atop an already visible {@link SidebarDrawer}. This component combines + * the {@link NestedSidebarDrawer} with a {@link SidebarDrawerTitlebar} and some + * standard spacing, so that the caller can just provide the content as the + * children. + */ +export const TitledNestedSidebarDrawer: React.FC< + React.PropsWithChildren< + NestedSidebarDrawerVisibilityProps & + Pick & + SidebarDrawerTitlebarProps + > +> = ({ open, onClose, onRootClose, anchor, children, ...rest }) => ( + + + + {children} + + +); diff --git a/web/packages/base/components/utils/mui-theme.d.ts b/web/packages/base/components/utils/mui-theme.d.ts index faeef7901c..1cdcdef9e2 100644 --- a/web/packages/base/components/utils/mui-theme.d.ts +++ b/web/packages/base/components/utils/mui-theme.d.ts @@ -186,7 +186,6 @@ declare module "@mui/material/Button" { success: false; info: false; warning: false; - inherit: false; // Add our custom palette colors. accent: true; critical: true; @@ -200,7 +199,6 @@ declare module "@mui/material/IconButton" { success: false; info: false; warning: false; - inherit: false; } } diff --git a/web/packages/base/components/utils/theme.ts b/web/packages/base/components/utils/theme.ts index 44c223c7ba..4792d4d8d4 100644 --- a/web/packages/base/components/utils/theme.ts +++ b/web/packages/base/components/utils/theme.ts @@ -552,11 +552,26 @@ const components: Components = { MuiPaper: { styleOverrides: { root: { - // MUI applies a semi-transparent background image for elevation - // in dark mode. Remove it to match background for our designs. - backgroundImage: "none", - // Use our paper shadow. - boxShadow: "var(--mui-palette-boxShadow-paper)", + variants: [ + { + // Use our "paper" shadow for elevated Paper. + props: { variant: "elevation" }, + style: { + // MUI applies a semi-transparent background image + // for elevation in dark mode. Remove it to match + // background for our designs. + backgroundImage: "none", + // Use our paper shadow. + boxShadow: "var(--mui-palette-boxShadow-paper)", + }, + }, + { + // Undo the effects of variant "elevation" case above + // case when elevation is 0. + props: { elevation: 0 }, + style: { boxShadow: "none" }, + }, + ], }, }, }, @@ -710,6 +725,13 @@ const components: Components = { }, }, }, + + MuiAlert: { + defaultProps: { + // Use the outlined variant by default (instead of "standard"). + variant: "outlined", + }, + }, }; // Exports --- diff --git a/web/packages/base/crypto/ente-impl.ts b/web/packages/base/crypto/ente-impl.ts index 27c8a3a688..e719a57d32 100644 --- a/web/packages/base/crypto/ente-impl.ts +++ b/web/packages/base/crypto/ente-impl.ts @@ -94,6 +94,12 @@ export const _decryptMetadataJSON = async (r: { r.keyB64, ); +export const _chunkHashInit = libsodium.chunkHashInit; + +export const _chunkHashUpdate = libsodium.chunkHashUpdate; + +export const _chunkHashFinal = libsodium.chunkHashFinal; + export const _generateKeyPair = libsodium.generateKeyPair; export const _boxSeal = libsodium.boxSeal; diff --git a/web/packages/base/crypto/libsodium.ts b/web/packages/base/crypto/libsodium.ts index b28b04720d..62d8fe1860 100644 --- a/web/packages/base/crypto/libsodium.ts +++ b/web/packages/base/crypto/libsodium.ts @@ -632,32 +632,71 @@ export async function decryptToUTF8( return sodium.to_string(decrypted); } -export async function initChunkHashing() { +/** + * An opaque object meant to be threaded through {@link chunkHashInit}, + * {@link chunkHashUpdate} and {@link chunkHashFinal}. + */ +export type ChunkHashState = sodium.StateAddress; + +/** + * Initialize and return new state that can be used to hash the chunks of data + * in a streaming manner. + * + * [Note: Chunked hashing] + * + * 1. Obtain a hash state using {@link chunkHashInit}. + * + * 2. Pass the hash state and the data chunk to {@link chunkHashUpdate}. + * + * 3. Finalize the hash state and obtain the hash using {@link chunkHashFinal}. + * + * @returns An opaque object representing the hash state. This can be passed + * (along with the data to hash) to {@link chunkHashUpdate}, and the final hash + * obtained using {@link chunkHashFinal}. + */ +export const chunkHashInit = async (): Promise => { await sodium.ready; - const hashState = sodium.crypto_generichash_init( + return sodium.crypto_generichash_init( null, sodium.crypto_generichash_BYTES_MAX, ); - return hashState; -} +}; -export async function hashFileChunk( - hashState: sodium.StateAddress, +/** + * Update the hash state to incorporate the contents of the provided data chunk. + * + * See: [Note: Chunked hashing] + * + * @param hashState A hash state obtained using {@link chunkHashInit}. + * + * @param chunk The data (bytes) to hash. + */ +export const chunkHashUpdate = async ( + hashState: ChunkHashState, chunk: Uint8Array, -) { +) => { await sodium.ready; sodium.crypto_generichash_update(hashState, chunk); -} +}; -export async function completeChunkHashing(hashState: sodium.StateAddress) { +/** + * Finalize a hash state and return the hash it represents (as a base64 string). + * + * See: [Note: Chunked hashing] + * + * @param hashState A hash state obtained using {@link chunkHashInit} and fed + * chunks using {@link chunkHashUpdate}. + * + * @returns The hash of all the chunks (as a base64 string). + */ +export const chunkHashFinal = async (hashState: ChunkHashState) => { await sodium.ready; const hash = sodium.crypto_generichash_final( hashState, sodium.crypto_generichash_BYTES_MAX, ); - const hashString = toB64(hash); - return hashString; -} + return toB64(hash); +}; /** * Generate a new public/private keypair for use with public-key encryption diff --git a/web/packages/base/crypto/worker.ts b/web/packages/base/crypto/worker.ts index 8e67927ecd..8e6db10341 100644 --- a/web/packages/base/crypto/worker.ts +++ b/web/packages/base/crypto/worker.ts @@ -1,6 +1,5 @@ import { expose } from "comlink"; import { logUnhandledErrorsAndRejectionsInWorker } from "ente-base/log-web"; -import type { StateAddress } from "libsodium-wrappers-sumo"; import * as ei from "./ente-impl"; import * as libsodium from "./libsodium"; @@ -39,6 +38,9 @@ export class CryptoWorker { decryptStreamChunk = ei._decryptStreamChunk; decryptMetadataJSON_New = ei._decryptMetadataJSON_New; decryptMetadataJSON = ei._decryptMetadataJSON; + chunkHashInit = ei._chunkHashInit; + chunkHashUpdate = ei._chunkHashUpdate; + chunkHashFinal = ei._chunkHashFinal; generateKeyPair = ei._generateKeyPair; boxSeal = ei._boxSeal; boxSealOpen = ei._boxSealOpen; @@ -48,18 +50,6 @@ export class CryptoWorker { // TODO: -- AUDIT BELOW -- - async initChunkHashing() { - return libsodium.initChunkHashing(); - } - - async hashFileChunk(hashState: StateAddress, chunk: Uint8Array) { - return libsodium.hashFileChunk(hashState, chunk); - } - - async completeChunkHashing(hashState: StateAddress) { - return libsodium.completeChunkHashing(hashState); - } - async decryptB64(data: string, nonce: string, key: string) { return libsodium.decryptB64(data, nonce, key); } diff --git a/web/packages/base/http.ts b/web/packages/base/http.ts index 6a12185c56..6ce7fb6e1f 100644 --- a/web/packages/base/http.ts +++ b/web/packages/base/http.ts @@ -1,3 +1,4 @@ +import { desktopAppVersion, isDesktop } from "ente-base/app"; import { wait } from "ente-utils/promise"; import { z } from "zod"; import { clientPackageName } from "./app"; @@ -14,16 +15,19 @@ import log from "./log"; export const authenticatedRequestHeaders = async () => ({ "X-Auth-Token": await ensureAuthToken(), "X-Client-Package": clientPackageName, + ...(isDesktop ? { "X-Client-Version": desktopAppVersion } : {}), }); /** * Return headers that should be passed alongwith (almost) all unauthenticated - * `fetch` calls that we make to our API servers. + * `fetch` calls that we make to our remotes like our API servers (museum), or + * to pre-signed URLs that are handled by the S3 storage buckets themselves. * * - The client package name. */ export const publicRequestHeaders = () => ({ "X-Client-Package": clientPackageName, + ...(isDesktop ? { "X-Client-Version": desktopAppVersion } : {}), }); /** @@ -164,16 +168,46 @@ export const isMuseumHTTPError = async ( return false; }; +interface RetryAsyncOperationOpts { + /** + * An optional modification to the default wait times between retries. + * + * - default 1 orig + 3 retries (2s, 5s, 10s) + * - "background" 1 orig + 3 retries (5s, 25s, 120s) + * + * default is fine for most operations, including interactive ones where we + * don't want to wait too long before giving up. "background" is suitable + * for non-interactive operations where we can wait for longer (thus better + * handle remote hiccups) without degrading the user's experience. + */ + retryProfile?: "background"; + /** + * An optional function that is called with the corresponding error whenever + * {@link op} rejects. It should throw the error if the retries should + * immediately be aborted. + */ + abortIfNeeded?: (error: unknown) => void; +} + /** - * Retry a async operation like a HTTP request 3 (+ 1 original) times with - * exponential backoff. + * Retry a async operation on failure up to 4 times (1 original + 3 retries) + * with exponential backoff. + * + * [Note: Retries of network requests should be idempotent] + * + * When dealing with network requests, avoid using this function directly, use + * one of its wrappers like {@link retryEnsuringHTTPOk} instead. Those wrappers + * ultimately use this function only, and there is nothing wrong with this + * function generally, however since this function allows retrying arbitrary + * promises, it is easy accidentally try and attempt retries of non-idemponent + * requests, while the more restricted API of {@link retryEnsuringHTTPOk} and + * other {@link HTTPRequestRetrier}s makes such misuse less likely. * * @param op A function that performs the operation, returning the promise for * its completion. * - * @param abortIfNeeded An optional function that is called with the - * corresponding error whenever {@link op} rejects. It should throw the error if - * the retries should immediately be aborted. + * @param opts Optional tweaks to the default implementation. See + * {@link RetryAsyncOperationOpts}. * * @returns A promise that fulfills with to the result of a first successfully * fulfilled promise of the 4 (1 + 3) attempts, or rejects with the error @@ -182,9 +216,13 @@ export const isMuseumHTTPError = async ( */ export const retryAsyncOperation = async ( op: () => Promise, - abortIfNeeded?: (error: unknown) => void, + opts?: RetryAsyncOperationOpts, ): Promise => { - const waitTimeBeforeNextTry = [2000, 5000, 10000]; + const { retryProfile, abortIfNeeded } = opts ?? {}; + const waitTimeBeforeNextTry = + retryProfile == "background" + ? [10000, 30000, 120000] + : [2000, 5000, 10000]; while (true) { try { @@ -201,35 +239,59 @@ export const retryAsyncOperation = async ( } }; +/** + * A function that wraps the request(s) in retries if needed. + * + * See {@link retryEnsuringHTTPOk} for the canonical example. This typedef is to + * allow us to talk about and pass functions that behave similar to + * {@link retryEnsuringHTTPOk}, but perhaps with other additional checks. + * + * See also: [Note: Retries of network requests should be idempotent] + */ +export type HTTPRequestRetrier = ( + request: () => Promise, + opts?: HTTPRequestRetrierOpts, +) => Promise; + +type HTTPRequestRetrierOpts = Pick; + /** * A helper function to adapt {@link retryAsyncOperation} for HTTP fetches. * - * This will ensure that the HTTP operation returning a non-200 OK status (as - * matched by {@link ensureOk}) is also counted as an error when considering if - * a request should be retried. + * This extends {@link retryAsyncOperation} by treating any non-200 OK status + * (as matched by {@link ensureOk}) as an error that should be retried. */ -export const retryEnsuringHTTPOk = (request: () => Promise) => +export const retryEnsuringHTTPOk: HTTPRequestRetrier = ( + request: () => Promise, + opts?: HTTPRequestRetrierOpts, +) => retryAsyncOperation(async () => { const r = await request(); ensureOk(r); return r; - }); + }, opts); /** - * A helper function to {@link retryAsyncOperation} for HTTP fetches, but - * considering any 4xx HTTP responses as irrecoverable failures. + * A helper function to adapt {@link retryAsyncOperation} for HTTP fetches, but + * treating any 4xx HTTP responses as irrecoverable failures. * * This is similar to {@link retryEnsuringHTTPOk}, except it stops retrying if * remote responds with a 4xx HTTP status. */ -export const retryEnsuringHTTPOkOr4xx = (request: () => Promise) => +export const retryEnsuringHTTPOkOr4xx: HTTPRequestRetrier = ( + request: () => Promise, + opts?: HTTPRequestRetrierOpts, +) => retryAsyncOperation( async () => { const r = await request(); ensureOk(r); return r; }, - (e) => { - if (isHTTP4xxError(e)) throw e; + { + ...opts, + abortIfNeeded(e) { + if (isHTTP4xxError(e)) throw e; + }, }, ); diff --git a/web/packages/base/kv.ts b/web/packages/base/kv.ts index 5fa20fb4ce..df90aca537 100644 --- a/web/packages/base/kv.ts +++ b/web/packages/base/kv.ts @@ -136,7 +136,7 @@ export const getKV = async (key: string) => { return db.get("kv", key); }; -export const _getKV = async ( +const _getKV = async ( key: string, type: string, ): Promise => { diff --git a/web/packages/base/local-user.ts b/web/packages/base/local-user.ts index 9bde1583a4..4552b07959 100644 --- a/web/packages/base/local-user.ts +++ b/web/packages/base/local-user.ts @@ -26,7 +26,9 @@ export type LocalUser = z.infer; * Return the logged-in user, if someone is indeed logged in. Otherwise return * `undefined`. * - * The user's data is stored in the browser's localStorage. + * The user's data is stored in the browser's localStorage. Thus, this function + * only works from the main thread, not from web workers (local storage is not + * accessible to web workers). */ export const localUser = (): LocalUser | undefined => { // TODO: duplicate of getData("user") @@ -45,19 +47,25 @@ export const ensureLocalUser = (): LocalUser => { }; /** - * Return the user's auth token, or throw an error. + * Return the user's auth token, if present. * * The user's auth token is stored in KV DB after they have successfully logged * in. This function returns that saved auth token. * - * If no such token is found (which should only happen if the user is not logged - * in), then it throws an error. - * * The underlying data is stored in IndexedDB, and can be accessed from web * workers. */ +export const getAuthToken = () => getKVS("token"); + +/** + * Return the user's auth token, or throw an error. + * + * The user's auth token can be retrieved using {@link getAuthToken}. This + * function is a wrapper which throws an error if the token is not found (which + * should only happen if the user is not logged in). + */ export const ensureAuthToken = async () => { - const token = await getKVS("token"); + const token = await getAuthToken(); if (!token) throw new Error("Not logged in"); return token; }; diff --git a/web/packages/base/locales/ar-SA/translation.json b/web/packages/base/locales/ar-SA/translation.json index 477847b531..b0ddd40cf1 100644 --- a/web/packages/base/locales/ar-SA/translation.json +++ b/web/packages/base/locales/ar-SA/translation.json @@ -32,7 +32,7 @@ "set_password": "تعيين كلمة المرور", "sign_in": "تسجيل الدخول", "incorrect_password": "كلمة المرور غير صحيحة", - "incorrect_password_or_no_account": "", + "incorrect_password_or_no_account": "كلمة المرور غير صحيحة أو البريد الإلكتروني غير مسجل", "pick_password_hint": "الرجاء إدخال كلمة المرور التي يمكننا استخدامها لتشفير بياناتك", "pick_password_caution": "نحن لا نخزن كلمة مرورك، لذا إذا نسيتها، لن نتمكن من مساعدتك في استرداد بياناتك دون مفتاح الاسترداد.", "key_generation_in_progress": "جارٍ توليد مفاتيح التشفير...", @@ -40,11 +40,12 @@ "referral_source_hint": "كيف سمعت عن Ente؟ (اختياري)", "referral_source_info": "نحن لا نتتبع عمليات تثبيت التطبيق، سيكون من المفيد لنا أن تخبرنا أين وجدتنا!", "password_mismatch_error": "كلمات المرور غير متطابقة", + "show_or_hide_password": "إظهار أو إخفاء كلمة المرور", "welcome_to_ente_title": "مرحبًا بك في ", "welcome_to_ente_subtitle": "تخزين الصور ومشاركتها بشكل مشفر من طرف إلى طرف", "new_album": "ألبوم جديد", "create_albums": "إنشاء ألبومات", - "enter_album_name": "اسم الألبوم", + "album_name": "اسم الألبوم", "close": "إغلاق", "yes": "نعم", "no": "لا", @@ -57,8 +58,8 @@ "add_photos_count": "إضافة {{count, number}} عناصر", "select_photos": "تحديد الصور", "file_upload": "تحميل الملف", - "preparing": "", - "processed_counts": "", + "preparing": "جاري التحضير", + "processed_counts": "{{count, number}} / {{total, number}}", "upload_reading_metadata_files": "", "upload_cancelling": "إلغاء التحميلات المتبقية", "upload_done": "", @@ -625,6 +626,7 @@ "faster_upload_description": "توجيه التحميلات عبر خوادم قريبة", "open_ente_on_startup": "فتح Ente عند بدء التشغيل", "cast_album_to_tv": "تشغيل الألبوم على التلفزيون", + "cast_to_tv": "", "enter_cast_pin_code": "أدخل الرمز الذي تراه على التلفزيون أدناه لإقران هذا الجهاز.", "code": "الرمز", "pair_device_to_tv": "إقران الأجهزة", @@ -675,5 +677,11 @@ "theme": "المظهر", "system": "النظام", "light": "فاتح", - "dark": "داكن" + "dark": "داكن", + "streamable_videos": "فيديوهات قابلة للبث", + "processing_videos_status": "معالجة الفيديوهات...", + "share_favorites": "مشاركة المفضلات", + "person_favorites": "مفضلات {{name}}", + "shared_favorites": "", + "added_by_name": "أضيفت بواسطة {{name}}" } diff --git a/web/packages/base/locales/be-BY/translation.json b/web/packages/base/locales/be-BY/translation.json index 033926ca0a..7cbcdd6cd6 100644 --- a/web/packages/base/locales/be-BY/translation.json +++ b/web/packages/base/locales/be-BY/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "Закрыць", "yes": "", "no": "Не", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/bg-BG/translation.json b/web/packages/base/locales/bg-BG/translation.json index 91d766ee22..40154f7807 100644 --- a/web/packages/base/locales/bg-BG/translation.json +++ b/web/packages/base/locales/bg-BG/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/ca-ES/translation.json b/web/packages/base/locales/ca-ES/translation.json index a1f96136ef..f2a3bd8d57 100644 --- a/web/packages/base/locales/ca-ES/translation.json +++ b/web/packages/base/locales/ca-ES/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/cs-CZ/translation.json b/web/packages/base/locales/cs-CZ/translation.json index 32656fb747..d7c9291f86 100644 --- a/web/packages/base/locales/cs-CZ/translation.json +++ b/web/packages/base/locales/cs-CZ/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "Jak jste se dozvěděli o Ente? (volitelné)", "referral_source_info": "Ne sledujeme instalace aplikace. Pomůže nám, když nám sdělíte, kde jste nás našli!", "password_mismatch_error": "Hesla se neshodují", + "show_or_hide_password": "", "welcome_to_ente_title": "Vítejte v ", "welcome_to_ente_subtitle": "End to end šifrované úložiště fotek a sdílení", "new_album": "Nové album", "create_albums": "Vytvořit alba", - "enter_album_name": "Název alba", + "album_name": "Název alba", "close": "Zavřít", "yes": "Ano", "no": "Ne", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "Přehrát album v televizi", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "Kód", "pair_device_to_tv": "Párovat zařízení", @@ -675,5 +677,11 @@ "theme": "Téma", "system": "Systém", "light": "Světlý", - "dark": "Tmavý" + "dark": "Tmavý", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/da-DK/translation.json b/web/packages/base/locales/da-DK/translation.json index 615b4b15f0..5889bd3eb1 100644 --- a/web/packages/base/locales/da-DK/translation.json +++ b/web/packages/base/locales/da-DK/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/de-DE/translation.json b/web/packages/base/locales/de-DE/translation.json index 7882443dd9..01bf96c65e 100644 --- a/web/packages/base/locales/de-DE/translation.json +++ b/web/packages/base/locales/de-DE/translation.json @@ -12,15 +12,15 @@ "enter_email": "E-Mail-Adresse eingeben", "invalid_email_error": "Geben Sie eine gültige E-Mail-Adresse ein", "required": "Erforderlich", - "email_not_registered": "", - "email_already_registered": "", + "email_not_registered": "E-Mail nicht registriert", + "email_already_registered": "E-Mail bereits registriert", "email_sent": "Bestätigungscode an {{email}} gesendet", "check_inbox_hint": "Bitte überprüfe deinen E-Mail-Posteingang (und Spam), um die Verifizierung abzuschließen", "verification_code": "Bestätigungscode", "resend_code": "Code erneut senden", "verify": "Überprüfen", "send_otp": "OTP senden", - "generic_error": "", + "generic_error": "Etwas ist schiefgelaufen", "generic_error_retry": "Ein Fehler ist aufgetreten, bitte versuche es erneut", "invalid_code_error": "Falscher Bestätigungscode", "expired_code_error": "Ihr Bestätigungscode ist abgelaufen", @@ -32,7 +32,7 @@ "set_password": "Passwort setzen", "sign_in": "Einloggen", "incorrect_password": "Falsches Passwort", - "incorrect_password_or_no_account": "", + "incorrect_password_or_no_account": "Falsches Passwort oder E-Mail nicht registriert", "pick_password_hint": "Bitte gib ein Passwort ein, mit dem wir deine Daten verschlüsseln können", "pick_password_caution": "Wir speichern dein Passwort nicht. Wenn du es vergisst, können wir dir nicht helfen, deine Daten ohne einen Wiederherstellungsschlüssel wiederherzustellen.", "key_generation_in_progress": "Generierung von Verschlüsselungsschlüsseln...", @@ -40,15 +40,16 @@ "referral_source_hint": "Wie hast du von Ente erfahren? (optional)", "referral_source_info": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!", "password_mismatch_error": "Die Passwörter stimmen nicht überein", + "show_or_hide_password": "Passwort ein- oder ausblenden", "welcome_to_ente_title": "Willkommen bei ", "welcome_to_ente_subtitle": "Ende-zu-Ende verschlüsselte Fotospeicherung und Freigabe", "new_album": "Neues Album", "create_albums": "Alben erstellen", - "enter_album_name": "Albumname", + "album_name": "Albumname", "close": "Schließen", - "yes": "", + "yes": "Ja", "no": "Nein", - "nothing_here": "", + "nothing_here": "Hier gibt es noch nichts", "upload": "Hochladen", "import": "Importieren", "add_photos": "Fotos hinzufügen", @@ -57,53 +58,53 @@ "add_photos_count": "{{count, number}} Dateien hinzufügen", "select_photos": "Foto auswählen", "file_upload": "Datei hochladen", - "preparing": "", - "processed_counts": "", - "upload_reading_metadata_files": "", + "preparing": "Wird vorbereitet", + "processed_counts": "{{count, number}} / {{total, number}}", + "upload_reading_metadata_files": "Metadaten werden gelesen", "upload_cancelling": "Verbleibende Uploads werden abgebrochen", - "upload_done": "", - "upload_skipped": "", + "upload_done": "{{count, number}} hochgeladen", + "upload_skipped": "{{count, number}} übersprungen", "initial_load_delay_warning": "Das erste Laden kann einige Zeit in Anspruch nehmen", "no_account": "Kein Konto vorhanden", "existing_account": "Es ist bereits ein Account vorhanden", "create": "Erstellen", - "files_count": "", + "files_count": "{{count, number}} Dateien", "download": "Herunterladen", "download_album": "Album herunterladen", "download_favorites": "Favoriten herunterladen", "download_uncategorized": "Download unkategorisiert", "download_hidden_items": "Versteckte Dateien herunterladen", - "audio": "", - "more": "", - "mouse_scroll": "", - "pan": "", - "pinch": "", - "drag": "", - "tap_inside_image": "", - "tap_outside_image": "", - "shortcuts": "", - "show_shortcuts": "", - "zoom_preset": "", - "toggle_controls": "", - "toggle_live": "", - "toggle_audio": "", - "toggle_favorite": "", - "toggle_archive": "", - "view_info": "", + "audio": "Audio", + "more": "Mehr", + "mouse_scroll": "Mausscrollen", + "pan": "Schwenken", + "pinch": "Kneifen", + "drag": "Ziehen", + "tap_inside_image": "Innerhalb des Bildes tippen", + "tap_outside_image": "Außerhalb des Bildes tippen", + "shortcuts": "Abkürzungen", + "show_shortcuts": "Abkürzungen anzeigen", + "zoom_preset": "Zoom Standard", + "toggle_controls": "Steuerelemente umschalten", + "toggle_live": "Live umschalten", + "toggle_audio": "Audio umschalten", + "toggle_favorite": "Favorit umschalten", + "toggle_archive": "Archiv umschalten", + "view_info": "Information anzeigen", "copy_as_png": "Als PNG kopieren", "toggle_fullscreen": "Vollbild umschalten", - "exit_fullscreen": "", - "go_fullscreen": "", - "zoom": "", - "play": "", - "pause": "", + "exit_fullscreen": "Vollbildmodus verlassen", + "go_fullscreen": "Vollbildmodus öffnen", + "zoom": "Zoom", + "play": "Wiedergeben", + "pause": "Pausieren", "previous": "Vorherige", "next": "Weitere", - "video_seek": "", - "quality": "", - "auto": "", - "original": "", - "speed": "", + "video_seek": "Video suchen", + "quality": "Qualität", + "auto": "Automatisch", + "original": "Original", + "speed": "Geschwindigkeit", "title_photos": "Ente Fotos", "title_auth": "Ente Auth", "title_accounts": "Ente Konten", @@ -118,7 +119,7 @@ "selected_count": "{{selected, number}} ausgewählt", "selected_and_yours_count": "{{selected, number}} ausgewählt {{yours, number}} von dir", "delete": "Löschen", - "favorite": "", + "favorite": "Favorit", "convert": "Konvertieren", "multi_folder_upload": "Mehrere Ordner erkannt", "upload_to_choice": "Möchtest du sie hochladen in", @@ -131,7 +132,7 @@ "password_changed_elsewhere": "Passwort anderswo geändert", "password_changed_elsewhere_message": "Bitte melde dich erneut auf diesem Gerät an, um dein neues Passwort zur Authentifizierung zu verwenden.", "go_back": "Zurück", - "account": "", + "account": "Konto", "recovery_key": "Wiederherstellungsschlüssel", "do_this_later": "Auf später verschieben", "save_key": "Schlüssel speichern", @@ -147,9 +148,9 @@ "no_recovery_key_message": "Aufgrund unseres Ende-zu-Ende-Verschlüsselungsprotokolls können Ihre Daten nicht ohne Ihr Passwort oder Ihren Wiederherstellungsschlüssel entschlüsselt werden", "no_two_factor_recovery_key_message": "Bitte sende eine E-Mail an {{emailID}} von deiner registrierten E-Mail-Adresse", "contact_support": "Support kontaktieren", - "help": "", - "ente_help": "", - "blog": "", + "help": "Hilfe", + "ente_help": "Entes Hilfe", + "blog": "Blog", "request_feature": "Feature anfragen", "support": "Support", "cancel": "Abbrechen", @@ -225,10 +226,10 @@ "keep_photos": "Fotos behalten", "share_album": "Album teilen", "sharing_with_self": "Du kannst nicht mit dir selbst teilen", - "sharing_already_shared": "", + "sharing_already_shared": "Sie teilen dies bereits mit {{email}}", "sharing_album_not_allowed": "Albumfreigabe nicht erlaubt", "sharing_disabled_for_free_accounts": "Freigabe ist für kostenlose Konten deaktiviert", - "sharing_user_does_not_exist": "", + "sharing_user_does_not_exist": "Kein Benutzer mit dieser E-Mail gefunden", "search": "Suchen", "search_results": "Ergebnisse durchsuchen", "no_results": "Keine Ergebnisse gefunden", @@ -244,29 +245,29 @@ "terms_and_conditions": "Ich stimme den Bedingungen und Datenschutzrichtlinien zu", "people": "Personen", "indexing_scheduled": "Indizierung ist geplant...", - "indexing_photos": "", - "indexing_fetching": "", - "indexing_people": "", - "syncing_wait": "", - "people_empty_too_few": "", - "unnamed_person": "", - "add_a_name": "", - "new_person": "", - "add_name": "", - "rename_person": "", - "reset_person_confirm": "", - "reset_person_confirm_message": "", - "ignore": "", - "ignore_person_confirm": "", - "ignore_person_confirm_message": "", - "ignored": "", - "show_person": "", - "review_suggestions": "", - "saved_choices": "", - "discard_changes": "", - "discard_changes_confirm_message": "", - "people_suggestions_finding": "", - "people_suggestions_empty": "", + "indexing_photos": "Aktualisiere Indexe...", + "indexing_fetching": "Synchronisiere Indexe...", + "indexing_people": "Synchronisiere Personen...", + "syncing_wait": "Synchronisiere...", + "people_empty_too_few": "Personen werden hier gezeigt, wenn genügend Fotos einer Person existieren", + "unnamed_person": "Unbenannte Person", + "add_a_name": "Name hinzufügen", + "new_person": "Neue Person", + "add_name": "Name hinzufügen", + "rename_person": "Person umbenennen", + "reset_person_confirm": "Person zurücksetzen?", + "reset_person_confirm_message": "Der Name, Gesichtsgruppen und Vorschläge für diese Person werden zurückgesetzt", + "ignore": "Ignorieren", + "ignore_person_confirm": "Person ignorieren?", + "ignore_person_confirm_message": "Diese Gesichtsgruppierung wird nicht in der Personenliste angezeigt werden", + "ignored": "Ignoriert", + "show_person": "Person anzeigen", + "review_suggestions": "Vorschläge überprüfen", + "saved_choices": "Gespeicherte Optionen", + "discard_changes": "Änderungen verwerfen", + "discard_changes_confirm_message": "Sie haben ungespeicherte Änderungen. Diese gehen verloren, wenn Sie schließen ohne zu Speichern", + "people_suggestions_finding": "Ähnliche Gesichter werden gefunden...", + "people_suggestions_empty": "Momentan keine weiteren Vorschläge", "info": "Info", "file_name": "Dateiname", "caption_placeholder": "Eine Beschreibung hinzufügen", @@ -309,7 +310,7 @@ "faq": "FAQ", "takeout_hint": "Entpacke alle Zip-Dateien in den gleichen Ordner und lade das hoch. Oder lade die Zip-Datei direkt hoch. Siehe FAQ für Details.", "destination": "Zielort", - "start": "", + "start": "Start", "last_export_time": "Letztes Exportdatum", "export_again": "Neusynchronisation", "local_storage_not_accessible": "Ihr Browser oder ein Addon blockiert Ente vor der Speicherung von Daten im lokalen Speicher", @@ -326,7 +327,7 @@ "unsupported_files": "Nicht unterstützte Dateien", "unsupported_files_hint": "Ente unterstützt diese Dateiformate noch nicht", "blocked_uploads": "Blockierte Uploads", - "blocked_uploads_hint": "", + "blocked_uploads_hint": "Ihr Browser oder ein Add-on hindert Ente daran, große Dateien mit eTags hochzuladen.", "large_files": "Große Dateien", "large_files_hint": "Diese Dateien wurden nicht hochgeladen, da sie unsere maximale Dateigröße überschreiten", "insufficient_storage": "Zu wenig Speicher", @@ -371,16 +372,16 @@ "confirm_remove_incl_others_message": "Einige der Elemente, die du entfernst, wurden von anderen Nutzern hinzugefügt und du wirst den Zugriff auf sie verlieren.", "oldest": "Ältestem", "last_updated": "Zuletzt aktualisiert", - "name": "", + "name": "Name", "fix_creation_time": "Zeit reparieren", "fix_creation_time_in_progress": "Zeit wird repariert", "fix_creation_time_file_updated": "Datei-Zeit aktualisiert", "fix_creation_time_completed": "Alle Dateien erfolgreich aktualisiert", "fix_creation_time_completed_with_errors": "Aktualisierung der Dateizeit für einige Dateien fehlgeschlagen, bitte versuche es erneut", "fix_creation_time_options": "Wählen Sie die Option, die Sie verwenden möchten", - "exif_date_time_original": "", - "exif_date_time_digitized": "", - "exif_metadata_date": "", + "exif_date_time_original": "Exif:DateTimeOriginal", + "exif_date_time_digitized": "Exif:DateTimeDigitized", + "exif_metadata_date": "Exif:MetadataDate", "custom_time": "Benutzerdefinierte Zeit", "caption_character_limit": "Maximal 5000 Zeichen", "sharing_details": "Details teilen", @@ -413,9 +414,9 @@ "or_add_existing": "Oder eine Vorherige auswählen", "not_found": "404 - Nicht gefunden", "link_expired": "Link ist abgelaufen", - "link_expired_message": "", + "link_expired_message": "Dieser Link ist entweder abgelaufen oder wurde deaktiviert", "manage_link": "Link verwalten", - "link_request_limit_exceeded": "", + "link_request_limit_exceeded": "Dieses Album wurde auf zu vielen Geräten angesehen", "allow_downloads": "Downloads erlauben", "allow_adding_photos": "Hinzufügen von Fotos erlauben", "allow_adding_photos_hint": "Erlaube Personen mit diesem Link, Fotos zum gemeinsamen Album hinzuzufügen.", @@ -450,13 +451,13 @@ "folder": "Ordner", "google_takeout": "Google Takeout", "deduplicate_files": "Duplikate bereinigen", - "remove_duplicates": "", - "total_size": "", - "count": "", - "deselect_all": "", - "no_duplicates": "", - "duplicate_group_description": "", - "remove_duplicates_button_count": "", + "remove_duplicates": "Duplikate entfernen", + "total_size": "Gesamtgröße", + "count": "Anzahl", + "deselect_all": "Auswahl aufheben", + "no_duplicates": "Keine Duplikate", + "duplicate_group_description": "{{count}} Elemente, {{itemSize}} pro", + "remove_duplicates_button_count": "Löschen Sie {{count, number}} Elemente", "stop_uploads_title": "Hochladen stoppen?", "stop_uploads_message": "Bist du sicher, dass du alle laufenden Uploads abbrechen möchtest?", "yes_stop_uploads": "Ja, Hochladen stoppen", @@ -493,8 +494,8 @@ "stop_watching_folder_message": "Deine bestehenden Dateien werden nicht gelöscht, aber das verknüpfte Ente-Album wird bei Änderungen in diesem Ordner nicht mehr aktualisiert.", "yes_stop": "Ja, Stopp", "change_folder": "Ordner ändern", - "view_logs": "", - "view_logs_message": "", + "view_logs": "Protokolle anzeigen", + "view_logs_message": "

Dies wird Debug-Protokolle anzeigen, welche Sie uns per E-Mail zusenden können, um ihr Problem zu analysieren.

Beachten Sie, dass Dateinamen enthalten sind um Probleme mit spezifischen Dateien nachvollziehen zu können.

", "weak_device_hint": "Dein Browser ist nicht leistungsstark genug, um deine Bilder zu verschlüsseln. Versuche, dich an einem Computer bei Ente anzumelden, oder lade dir die Ente-App für dein Gerät (Handy oder Desktop) herunter.", "drag_and_drop_hint": "Oder ziehe Dateien per Drag-and-Drop in das Ente-Fenster", "authenticate": "Authentifizieren", @@ -545,7 +546,7 @@ "gb": "GB", "tb": "TB" }, - "stop": "", + "stop": "Stopp", "sync_continuously": "Stets aktuell halten", "export_starting": "Starte Export...", "export_preparing": "Vorbereiten...", @@ -573,7 +574,7 @@ "at": "um", "auth_next": "Weiter", "auth_download_mobile_app": "Lade unsere smartphone App herunter, um deine Schlüssel zu verwalten", - "no_codes_added_yet": "", + "no_codes_added_yet": "Noch keine Codes hinzugefügt", "hide": "Ausblenden", "unhide": "Einblenden", "sort_by": "Sortieren nach", @@ -593,8 +594,8 @@ "image": "Bild", "video": "Video", "live_photo": "Live-foto", - "live": "", - "edit_image": "", + "live": "Live", + "edit_image": "Bild bearbeiten", "photo_editor": "Foto-editor", "confirm_editor_close": "Editor wirklich schließen?", "confirm_editor_close_message": "Lade dein bearbeitetes Bild herunter oder speichere es in Ente, um die Änderungen nicht zu verlieren.", @@ -623,10 +624,11 @@ "reset": "Zurücksetzen", "faster_upload": "Schnelleres Hochladen", "faster_upload_description": "Uploads über nahegelegene Server leiten", - "open_ente_on_startup": "", + "open_ente_on_startup": "Ente beim PC-Start ausführen", "cast_album_to_tv": "Album auf Fernseher wiedergeben", + "cast_to_tv": "", "enter_cast_pin_code": "Gib den Code auf dem Fernseher unten ein, um dieses Gerät zu koppeln.", - "code": "", + "code": "Code", "pair_device_to_tv": "Geräte koppeln", "tv_not_found": "Fernseher nicht gefunden. Hast du die PIN korrekt eingegeben?", "cast_auto_pair": "Automatisch verbinden", @@ -657,7 +659,7 @@ "check_status": "Status überprüfen", "passkey_login_instructions": "Folge den Schritten in deinem Browser, um mit dem Anmelden fortzufahren.", "passkey_login": "Mit Passkey anmelden", - "totp_login": "", + "totp_login": "Mit TOTP anmelden", "passkey": "Passkey", "passkey_verify_description": "Überprüfe deinen Passkey, um dich in dein Konto einzuloggen.", "waiting_for_verification": "Warte auf Bestätigung...", @@ -672,8 +674,14 @@ "server_endpoint": "Server Endpunkt", "more_information": "Weitere Informationen", "save": "Speichern", - "theme": "", - "system": "", - "light": "", - "dark": "" + "theme": "Stil", + "system": "System", + "light": "Hell", + "dark": "Dunkel", + "streamable_videos": "Streambare Videos", + "processing_videos_status": "Verarbeite Videos...", + "share_favorites": "Favoriten teilen", + "person_favorites": "{{name}}s Favoriten", + "shared_favorites": "Geteilte Favoriten", + "added_by_name": "Von {{name}} hinzugefügt" } diff --git a/web/packages/base/locales/el-GR/translation.json b/web/packages/base/locales/el-GR/translation.json index 04cf6dfe2d..05fb00d94d 100644 --- a/web/packages/base/locales/el-GR/translation.json +++ b/web/packages/base/locales/el-GR/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "Πώς ακούσατε για το Ente; (προαιρετικό)", "referral_source_info": "Δεν παρακολουθούμε τις εγκαταστάσεις εφαρμογών. Θα μας βοηθούσε αν μας λέγατε που μας βρήκατε!", "password_mismatch_error": "Οι κωδικοί πρόσβασης δεν ταιριάζουν", + "show_or_hide_password": "", "welcome_to_ente_title": "Καλώς ήρθατε στο ", "welcome_to_ente_subtitle": "", "new_album": "Νέο άλμπουμ", "create_albums": "", - "enter_album_name": "Όνομα άλμπουμ", + "album_name": "Όνομα άλμπουμ", "close": "Κλείσιμο", "yes": "", "no": "Όχι", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "Αναπαραγωγή άλμπουμ στην τηλεόραση", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "Ζεύξη συσκευών", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/en-US/translation.json b/web/packages/base/locales/en-US/translation.json index d27e7437bc..c596f05727 100644 --- a/web/packages/base/locales/en-US/translation.json +++ b/web/packages/base/locales/en-US/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "How did you hear about Ente? (optional)", "referral_source_info": "We don't track app installs, It'd help us if you told us where you found us!", "password_mismatch_error": "Passwords don't match", + "show_or_hide_password": "Show or hide password", "welcome_to_ente_title": "Welcome to ", "welcome_to_ente_subtitle": "End to end encrypted photo storage and sharing", "new_album": "New album", "create_albums": "Create albums", - "enter_album_name": "Album name", + "album_name": "Album name", "close": "Close", "yes": "Yes", "no": "No", @@ -625,6 +626,7 @@ "faster_upload_description": "Route uploads through nearby servers", "open_ente_on_startup": "Open Ente on startup", "cast_album_to_tv": "Play album on TV", + "cast_to_tv": "Play on TV", "enter_cast_pin_code": "Enter the code you see on the TV below to pair this device.", "code": "Code", "pair_device_to_tv": "Pair devices", @@ -675,5 +677,11 @@ "theme": "Theme", "system": "System", "light": "Light", - "dark": "Dark" + "dark": "Dark", + "streamable_videos": "Streamable videos", + "processing_videos_status": "Processing videos...", + "share_favorites": "Share favorites", + "person_favorites": "{{name}}'s favorites", + "shared_favorites": "Shared favorites", + "added_by_name": "Added by {{name}}" } diff --git a/web/packages/base/locales/es-ES/translation.json b/web/packages/base/locales/es-ES/translation.json index 3a6f7ac23c..5aaeb35480 100644 --- a/web/packages/base/locales/es-ES/translation.json +++ b/web/packages/base/locales/es-ES/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "¿Cómo conociste Ente? (opcional)", "referral_source_info": "No rastreamos la instalación de las aplicaciones. ¡Nos ayudaría si nos dijera dónde nos encontró!", "password_mismatch_error": "Las contraseñas no coinciden", + "show_or_hide_password": "", "welcome_to_ente_title": "Bienvenido a ", "welcome_to_ente_subtitle": "Almacenamiento y compartición de fotos cifradas de extremo a extremo", "new_album": "Nuevo álbum", "create_albums": "Crear álbumes", - "enter_album_name": "Nombre del álbum", + "album_name": "Nombre del álbum", "close": "Cerrar", "yes": "Sí", "no": "No", @@ -625,6 +626,7 @@ "faster_upload_description": "Enrutar subidas a través de servidores cercanos", "open_ente_on_startup": "Abrir ente al iniciar", "cast_album_to_tv": "Reproducir álbum en TV", + "cast_to_tv": "", "enter_cast_pin_code": "Introduce el código que ves en el televisor para emparejar este dispositivo.", "code": "Código", "pair_device_to_tv": "Emparejar dispositivos", @@ -675,5 +677,11 @@ "theme": "Tema", "system": "Sistema", "light": "Claro", - "dark": "Oscuro" + "dark": "Oscuro", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/et-EE/translation.json b/web/packages/base/locales/et-EE/translation.json index a1f96136ef..f2a3bd8d57 100644 --- a/web/packages/base/locales/et-EE/translation.json +++ b/web/packages/base/locales/et-EE/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/fa-IR/translation.json b/web/packages/base/locales/fa-IR/translation.json index 28345107dd..b3cb46480e 100644 --- a/web/packages/base/locales/fa-IR/translation.json +++ b/web/packages/base/locales/fa-IR/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "به خوش آمدید", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/fi-FI/translation.json b/web/packages/base/locales/fi-FI/translation.json index dd2859d139..960f83ad3c 100644 --- a/web/packages/base/locales/fi-FI/translation.json +++ b/web/packages/base/locales/fi-FI/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "Miten kuulit Entestä? (valinnainen)", "referral_source_info": "Emme seuraa sovelluksen asennuksia. Se auttaisi meitä, jos kertoisit mistä löysit meidät!", "password_mismatch_error": "Salasanat eivät täsmää", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "Päästä päähän salattu valokuvien tallennustila ja jakaminen", "new_album": "Uusi albumi", "create_albums": "Luo albumit", - "enter_album_name": "Albumin nimi", + "album_name": "Albumin nimi", "close": "Sulje", "yes": "Kyllä", "no": "Ei", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/fr-FR/translation.json b/web/packages/base/locales/fr-FR/translation.json index 53fcbdfe53..51c69fa55c 100644 --- a/web/packages/base/locales/fr-FR/translation.json +++ b/web/packages/base/locales/fr-FR/translation.json @@ -32,7 +32,7 @@ "set_password": "Définir le mot de passe", "sign_in": "Connexion", "incorrect_password": "Mot de passe non valide", - "incorrect_password_or_no_account": "", + "incorrect_password_or_no_account": "Mot de passe incorrect ou adresse email non enregistrée", "pick_password_hint": "Veuillez saisir un mot de passe que nous pourrons utiliser pour chiffrer vos données", "pick_password_caution": "Nous ne stockons pas votre mot de passe, donc si vous le perdez, nous ne pourrons pas vous aider à récupérer vos données sans une clé de récupération.", "key_generation_in_progress": "Génération des clés de chiffrement...", @@ -40,11 +40,12 @@ "referral_source_hint": "Comment avez-vous entendu parler de Ente? (facultatif)", "referral_source_info": "Nous ne suivons pas les installations d'applications. Il serait utile que vous nous disiez comment vous nous avez trouvés !", "password_mismatch_error": "Les mots de passe ne correspondent pas", + "show_or_hide_password": "Afficher/Masquer le mot de passe", "welcome_to_ente_title": "Bienvenue sur ", "welcome_to_ente_subtitle": "Stockage et partage de photos chiffrés de bout en bout", "new_album": "Nouvel album", "create_albums": "Créer des albums", - "enter_album_name": "Nom de l'album", + "album_name": "Nom de l'album", "close": "Fermer", "yes": "Oui", "no": "Non", @@ -57,12 +58,12 @@ "add_photos_count": "Ajouter {{count, number}} éléments", "select_photos": "Sélectionner des photos", "file_upload": "Fichier chargé", - "preparing": "", - "processed_counts": "", - "upload_reading_metadata_files": "", + "preparing": "En préparation", + "processed_counts": "{{count, number}} / {{total, number}}", + "upload_reading_metadata_files": "Lecture des fichiers de métadonnées", "upload_cancelling": "Annulation des chargements restants", - "upload_done": "", - "upload_skipped": "", + "upload_done": "{{count, number}} chargé", + "upload_skipped": "{{count, number}} ignoré(s)", "initial_load_delay_warning": "Le premier affichage peut prendre du temps", "no_account": "Je n'ai pas de compte", "existing_account": "J'ai déjà un compte", @@ -95,19 +96,19 @@ "exit_fullscreen": "Quitter le mode plein écran", "go_fullscreen": "Passer en plein écran", "zoom": "Zoom", - "play": "", - "pause": "", + "play": "Lecture", + "pause": "Pause", "previous": "Précédent", "next": "Suivant", - "video_seek": "", - "quality": "", - "auto": "", - "original": "", - "speed": "", + "video_seek": "Recherche vidéo", + "quality": "Qualité", + "auto": "Auto", + "original": "Original", + "speed": "Vitesse", "title_photos": "Ente Photos", "title_auth": "Ente Auth", "title_accounts": "Comptes Ente", - "upload_first_photo": "Chargez votre 1ere photo", + "upload_first_photo": "Chargez votre 1ère photo", "import_your_folders": "Importez vos dossiers", "upload_dropzone_hint": "Déposez pour sauvegarder vos fichiers", "watch_folder_dropzone_hint": "Déposez pour ajouter un dossier surveillé", @@ -625,6 +626,7 @@ "faster_upload_description": "Router les chargements vers les serveurs à proximité", "open_ente_on_startup": "Ouvrir Ente au démarrage", "cast_album_to_tv": "Jouer l'album sur la TV", + "cast_to_tv": "", "enter_cast_pin_code": "Entrez le code que vous voyez sur la TV ci-dessous pour appairer cet appareil.", "code": "Code", "pair_device_to_tv": "Associer les appareils", @@ -675,5 +677,11 @@ "theme": "Thème", "system": "Système", "light": "Clair", - "dark": "Sombre" + "dark": "Sombre", + "streamable_videos": "Vidéos diffusables", + "processing_videos_status": "Traitement des vidéos...", + "share_favorites": "Partager les favoris", + "person_favorites": "Favoris de {{name}}", + "shared_favorites": "Favoris partagés", + "added_by_name": "" } diff --git a/web/packages/base/locales/gu-IN/translation.json b/web/packages/base/locales/gu-IN/translation.json index a1f96136ef..f2a3bd8d57 100644 --- a/web/packages/base/locales/gu-IN/translation.json +++ b/web/packages/base/locales/gu-IN/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/hi-IN/translation.json b/web/packages/base/locales/hi-IN/translation.json index a1f96136ef..f2a3bd8d57 100644 --- a/web/packages/base/locales/hi-IN/translation.json +++ b/web/packages/base/locales/hi-IN/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/hu-HU/translation.json b/web/packages/base/locales/hu-HU/translation.json index a1f96136ef..f2a3bd8d57 100644 --- a/web/packages/base/locales/hu-HU/translation.json +++ b/web/packages/base/locales/hu-HU/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/id-ID/translation.json b/web/packages/base/locales/id-ID/translation.json index ed04f14705..7d9c34a043 100644 --- a/web/packages/base/locales/id-ID/translation.json +++ b/web/packages/base/locales/id-ID/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "Dari mana Anda menemukan Ente? (opsional)", "referral_source_info": "Kami tidak melacak pemasangan aplikasi, Ini akan membantu kami jika Anda memberi tahu kami di mana Anda menemukan kami!", "password_mismatch_error": "Sandi tidak cocok", + "show_or_hide_password": "", "welcome_to_ente_title": "Selamat datang di ", "welcome_to_ente_subtitle": "Penyimpanan dan berbagi foto terenkripsi ujung ke ujung", "new_album": "Album baru", "create_albums": "", - "enter_album_name": "Nama album", + "album_name": "Nama album", "close": "Tutup", "yes": "", "no": "Tidak", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "Putar album di TV", + "cast_to_tv": "", "enter_cast_pin_code": "Masukkan kode yang ditampilkan TV di bawah untuk menautkan perangkat ini.", "code": "", "pair_device_to_tv": "Tautkan perangkat", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/is-IS/translation.json b/web/packages/base/locales/is-IS/translation.json index 4b6cb00ce6..492ed88255 100644 --- a/web/packages/base/locales/is-IS/translation.json +++ b/web/packages/base/locales/is-IS/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "Loka", "yes": "", "no": "Nei", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/it-IT/translation.json b/web/packages/base/locales/it-IT/translation.json index 1160484ff2..7ea28bbc9d 100644 --- a/web/packages/base/locales/it-IT/translation.json +++ b/web/packages/base/locales/it-IT/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "Come hai conosciuto Ente? (opzionale)", "referral_source_info": "Non teniamo traccia delle installazioni dell'app. Ci sarebbe utile se ci dicessi come ci hai trovato!", "password_mismatch_error": "Le password non corrispondono", + "show_or_hide_password": "", "welcome_to_ente_title": "Benvenuto su ", "welcome_to_ente_subtitle": "Archiviazione e condivisione di foto crittografate end-to-end", "new_album": "Nuovo album", "create_albums": "Crea album", - "enter_album_name": "Nome album", + "album_name": "Nome album", "close": "Chiudi", "yes": "", "no": "No", @@ -77,8 +78,8 @@ "more": "", "mouse_scroll": "", "pan": "", - "pinch": "", - "drag": "", + "pinch": "Pizzica", + "drag": "Trascina", "tap_inside_image": "", "tap_outside_image": "", "shortcuts": "", @@ -625,6 +626,7 @@ "faster_upload_description": "Effettua l'upload attraverso i server vicini", "open_ente_on_startup": "", "cast_album_to_tv": "Riproduci album sulla TV", + "cast_to_tv": "", "enter_cast_pin_code": "Inserisci il codice che vedi sulla TV qui sotto per associare questo dispositivo.", "code": "", "pair_device_to_tv": "Associa dispositivi", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/ja-JP/translation.json b/web/packages/base/locales/ja-JP/translation.json index 2f1b26eccd..2ac58d4b97 100644 --- a/web/packages/base/locales/ja-JP/translation.json +++ b/web/packages/base/locales/ja-JP/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "Enteをどのように知りましたか?(任意)", "referral_source_info": "アプリのインストールを追跡していませんが、どこで私たちを見つけたか教えていただけると助かります!", "password_mismatch_error": "パスワードが一致しません", + "show_or_hide_password": "", "welcome_to_ente_title": " へようこそ", "welcome_to_ente_subtitle": "エンドツーエンド暗号化された写真の保存と共有", "new_album": "新しいアルバム", "create_albums": "アルバムを作成", - "enter_album_name": "アルバムの名前", + "album_name": "アルバムの名前", "close": "閉じる", "yes": "はい", "no": "いいえ", @@ -625,6 +626,7 @@ "faster_upload_description": "近くのサーバー経由でのアップロード", "open_ente_on_startup": "開始時に Ente を開く", "cast_album_to_tv": "TVでアルバムを再生", + "cast_to_tv": "", "enter_cast_pin_code": "このデバイスをペアリングするには、以下のテレビで表示されているコードを入力してください。", "code": "コード", "pair_device_to_tv": "デバイスを接続する", @@ -675,5 +677,11 @@ "theme": "テーマ", "system": "システム", "light": "ライト", - "dark": "ダーク" + "dark": "ダーク", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/km-KH/translation.json b/web/packages/base/locales/km-KH/translation.json index a1f96136ef..f2a3bd8d57 100644 --- a/web/packages/base/locales/km-KH/translation.json +++ b/web/packages/base/locales/km-KH/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/ko-KR/translation.json b/web/packages/base/locales/ko-KR/translation.json index 026732e3a6..c17949941d 100644 --- a/web/packages/base/locales/ko-KR/translation.json +++ b/web/packages/base/locales/ko-KR/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "Ente에 대해 어떻게 알게 되었나요? (선택 사항)", "referral_source_info": "저희는 앱 설치를 추적하지 않습니다. 어디서 저희를 찾으셨는지 알려주시면 도움이 될 거예요!", "password_mismatch_error": "비밀번호가 일치하지 않습니다", + "show_or_hide_password": "", "welcome_to_ente_title": "환영합니다 ", "welcome_to_ente_subtitle": "종간단 암호화된 사진 저장 및 공유", "new_album": "새 앨범", "create_albums": "앨범 만들기", - "enter_album_name": "앨범 이름", + "album_name": "앨범 이름", "close": "닫기", "yes": "예", "no": "아니요", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/lt-LT/translation.json b/web/packages/base/locales/lt-LT/translation.json index b4dcafcd08..212ccb36e4 100644 --- a/web/packages/base/locales/lt-LT/translation.json +++ b/web/packages/base/locales/lt-LT/translation.json @@ -27,12 +27,12 @@ "status_sending": "Siunčiama...", "status_sent": "Išsiųsta.", "password": "Slaptažodis", - "link_password_description": "Įveskite slaptažodį, kad atrakintumėte albumą", + "link_password_description": "Įveskite slaptažodį, kad atrakintumėte albumą.", "unlock": "Atrakinti", "set_password": "Nustatyti slaptažodį", "sign_in": "Prisijungti", "incorrect_password": "Neteisingas slaptažodis.", - "incorrect_password_or_no_account": "", + "incorrect_password_or_no_account": "Neteisingas slaptažodis arba neregistruotas el. paštas.", "pick_password_hint": "Įveskite slaptažodį, kurį galime naudoti jūsų duomenims šifruoti.", "pick_password_caution": "Jūsų slaptažodžio nesaugome, todėl jei jį pamiršite, be atkūrimo rakto negalėsime padėti atkurti duomenų.", "key_generation_in_progress": "Generuojami šifravimo raktai...", @@ -40,15 +40,16 @@ "referral_source_hint": "Kaip išgirdote apie „Ente“? (nebūtina)", "referral_source_info": "Mes nesekame programų diegimų. Mums padėtų, jei pasakytumėte, kur mus radote.", "password_mismatch_error": "Slaptažodžiai nesutampa.", + "show_or_hide_password": "", "welcome_to_ente_title": "Sveiki atvykę į „“", "welcome_to_ente_subtitle": "Visapusiškai užšifruota nuotraukų saugykla ir bendrinimas", "new_album": "Naujas albumas", "create_albums": "Kurti albumus", - "enter_album_name": "Albumo pavadinimas", + "album_name": "Albumo pavadinimas", "close": "Užverti", "yes": "Taip", "no": "Ne", - "nothing_here": "Kol kas nieko čia nėra", + "nothing_here": "Čia dar nieko nėra.", "upload": "Įkelti", "import": "Importuoti", "add_photos": "Įtraukti nuotraukų", @@ -63,7 +64,7 @@ "upload_cancelling": "Atšaukiami likę įkėlimai", "upload_done": "{{count, number}} įkelta", "upload_skipped": "{{count, number}} praleista", - "initial_load_delay_warning": "Pirmasis įkėlimas gali šiek tiek užtrukti", + "initial_load_delay_warning": "Pirmasis įkėlimas gali šiek tiek užtrukti.", "no_account": "Neturiu paskyros", "existing_account": "Jau turiu paskyrą", "create": "Kurti", @@ -71,7 +72,7 @@ "download": "Atsisiųsti", "download_album": "Atsisiųsti albumą", "download_favorites": "Atsisiųsti mėgstamus", - "download_uncategorized": "Atsisiųsti nekategorizuotas", + "download_uncategorized": "Atsisiųsti nekategorizuotus", "download_hidden_items": "Atsisiųsti paslėptus elementus", "audio": "Garsas", "more": "Daugiau", @@ -136,7 +137,7 @@ "do_this_later": "Daryti tai vėliau", "save_key": "Išsaugoti raktą", "recovery_key_description": "Jei pamiršote slaptažodį, vienintelis būdas atkurti duomenis – naudoti šį raktą.", - "key_not_stored_note": "Šio rakto mes nesaugome, todėl išsaugokite jį saugioje vietoje", + "key_not_stored_note": "Šio rakto mes nesaugome, todėl išsaugokite jį saugioje vietoje.", "recovery_key_generation_failed": "Atkūrimo kodo nepavyko sugeneruoti. Bandykite dar kartą.", "forgot_password": "Pamiršau slaptažodį", "recover_account": "Atkurti paskyrą", @@ -151,7 +152,7 @@ "ente_help": "„Ente“ pagalba", "blog": "Tinklaraštis", "request_feature": "Prašyti funkcijos", - "support": "Pagalba", + "support": "Pagalba el. paštu", "cancel": "Atšaukti", "logout": "Atsijungti", "logout_message": "Ar tikrai norite atsijungti?", @@ -178,7 +179,7 @@ "current_usage": "Dabartinis naudojimas – {{usage}}", "two_months_free": "Gaukite 2 mėnesius nemokamai metiniuose planuose", "free_plan_option": "Tęsti su nemokamu planu", - "free_plan_description": "{{storage}} nemokamai amžinai", + "free_plan_description": "{{storage}} nemokamai amžinai.", "active": "Aktyvi", "subscription_info_free": "Esate nemokame plane", "subscription_info_family": "Esate šeimos plane, kurį valdo", @@ -248,11 +249,11 @@ "indexing_fetching": "Sinchronizuojami indeksavimai...", "indexing_people": "Sinchronizuojami asmenys...", "syncing_wait": "Sinchronizuojama...", - "people_empty_too_few": "Asmenys čia bus rodomi, kai bus pakankamai asmens nuotraukų", + "people_empty_too_few": "Asmenys čia bus rodomi, kai bus pakankamai asmens nuotraukų.", "unnamed_person": "Neįvardytas asmuo", "add_a_name": "Pridėti vardą", "new_person": "Naujas asmuo", - "add_name": "Pridėti vardą", + "add_name": "Pridėkite vardą", "rename_person": "Pervadinti asmenį", "reset_person_confirm": "Nustatyti asmenį iš naujo?", "reset_person_confirm_message": "Vardas, veidų grupavimas ir pasiūlymai šiam asmeniui bus iš naujo nustatyti", @@ -269,7 +270,7 @@ "people_suggestions_empty": "Kol kas daugiau pasiūlymų nėra", "info": "Informacija", "file_name": "Failo pavadinimas", - "caption_placeholder": "Pridėti aprašymą", + "caption_placeholder": "Pridėkite aprašą", "location": "Vietovė", "view_on_map": "Peržiūrėti žemėlapyje „OpenStreetMap“", "map": "Žemėlapis", @@ -454,7 +455,7 @@ "total_size": "Bendrą dydį", "count": "Skaičių", "deselect_all": "Naikinti visų pasirinkimą", - "no_duplicates": "Dublikatų nėra", + "no_duplicates": "Dublikatų nėra.", "duplicate_group_description": "{{count}} elementai (-ų), kiekvienas {{itemSize}}", "remove_duplicates_button_count": "Ištrinti {{count, number}} elementus (-ų)", "stop_uploads_title": "Stabdyti įkėlimus?", @@ -502,7 +503,7 @@ "uploaded_to_separate_collections": "Įkelta į atskiras kolekcijas", "nevermind": "Nesvarbu", "update_available": "Yra naujinimas", - "update_installable_message": "Nauja „Ente“ versija jau paruošta įdiegti.", + "update_installable_message": "Nauja „Ente“ versija jau parengta įdiegti.", "install_now": "Diegti dabar", "install_on_next_launch": "Diegti kito paleidimo metu", "update_available_message": "Išleista nauja „Ente“ versija, bet jos negalima automatiškai atsisiųsti ir įdiegti.", @@ -511,12 +512,12 @@ "today": "Šiandien", "yesterday": "Vakar", "enter_name": "Įveskite vardą", - "uploader_name_hint": "Pridėkite vardą, kad draugai žinotų, kam padėkoti už šias puikias nuotraukas.", + "uploader_name_hint": "Pridėkite vardą, kad draugai žinotų, kam padėkoti už šias puikias nuotraukas!", "name_placeholder": "Pavadinimas...", "more_details": "Daugiau išsamios informacijos", "ml_search": "Mašininis mokymasis", - "ml_search_description": "„Ente“ palaiko įrenginyje mašininį mokymąsi, skirtą veidų atpažinimui, magiškai paieškai ir kitoms išplėstinėms paieškos funkcijoms", - "ml_search_footnote": "Magiška paieška leidžia ieškoti nuotraukų pagal jų turinį, pvz., „automobilis“, „raudonas automobilis“, „Ferrari“", + "ml_search_description": "„Ente“ palaiko įrenginyje mašininį mokymąsi, skirtą veidų atpažinimui, magiškai paieškai ir kitoms išplėstinėms paieškos funkcijoms.", + "ml_search_footnote": "Magiška paieška leidžia ieškoti nuotraukų pagal jų turinį, pvz., „automobilis“, „raudonas automobilis“, „Ferrari“.", "indexing": "Indeksavimas", "processed": "Apdorota", "indexing_status_running": "Vykdomas", @@ -573,9 +574,9 @@ "at": " ", "auth_next": "sekantis", "auth_download_mobile_app": "Atsisiųskite mūsų mobiliąją programą, kad tvarkytumėte savo paslaptis.", - "no_codes_added_yet": "Dar nėra pridėtų kodų", + "no_codes_added_yet": "Dar nėra pridėtų kodų.", "hide": "Slėpti", - "unhide": "Neslėpti", + "unhide": "Rodyti", "sort_by": "Rikiuoti pagal", "newest_first": "Naujausią pirmiausiai", "oldest_first": "Seniausią pirmiausiai", @@ -611,7 +612,7 @@ "rotation": "Sukimas", "rotate_left": "Sukti į kairę", "rotate_right": "Sukti į dešinę", - "flip": "Apversti", + "flip": "Apverstimas", "flip_vertically": "Apversti vertikaliai", "flip_horizontally": "Apversti horizontaliai", "download_edited": "Atsisiųsti redaguotą", @@ -623,8 +624,9 @@ "reset": "Atkurti", "faster_upload": "Spartesni įkėlimai", "faster_upload_description": "Nukreipkite įkėlimus per netoliese esančius serverius.", - "open_ente_on_startup": "Atverti „Ente“ paleidžiant", + "open_ente_on_startup": "Atverti „Ente“ paleidimo metu", "cast_album_to_tv": "Paleisti albumą televizoriuje", + "cast_to_tv": "", "enter_cast_pin_code": "Įveskite žemiau esančiame televizoriuje matomą kodą, kad susietumėte šį įrenginį.", "code": "Kodas", "pair_device_to_tv": "Susieti įrenginius", @@ -662,7 +664,7 @@ "passkey_verify_description": "Patvirtinkite savo slaptaraktį, kad prisijungtumėte prie paskyros.", "waiting_for_verification": "Laukiama patvirtinimo...", "verification_still_pending": "Vis dar laukiama patvirtinimo", - "passkey_verified": "Patvirtintas slaptaraktis", + "passkey_verified": "Slaptaraktis patvirtintas", "redirecting_back_to_app": "Nukreipiama atgal į programą...", "redirect_close_instructions": "Atvėrę programą galite užverti šį langą.", "redirect_again": "Nukreipti iš naujo", @@ -675,5 +677,11 @@ "theme": "Tema", "system": "Sistemos", "light": "Šviesi", - "dark": "Tamsi" + "dark": "Tamsi", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/lv-LV/translation.json b/web/packages/base/locales/lv-LV/translation.json index a1f96136ef..f2a3bd8d57 100644 --- a/web/packages/base/locales/lv-LV/translation.json +++ b/web/packages/base/locales/lv-LV/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/ml-IN/translation.json b/web/packages/base/locales/ml-IN/translation.json index a1f96136ef..f2a3bd8d57 100644 --- a/web/packages/base/locales/ml-IN/translation.json +++ b/web/packages/base/locales/ml-IN/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/nl-NL/translation.json b/web/packages/base/locales/nl-NL/translation.json index a475623b29..17356a9c97 100644 --- a/web/packages/base/locales/nl-NL/translation.json +++ b/web/packages/base/locales/nl-NL/translation.json @@ -32,7 +32,7 @@ "set_password": "Wachtwoord instellen", "sign_in": "Aanmelden", "incorrect_password": "Onjuist wachtwoord", - "incorrect_password_or_no_account": "", + "incorrect_password_or_no_account": "Onjuist wachtwoord of e-mailadres niet geregistreerd", "pick_password_hint": "Voer een wachtwoord in dat we kunnen gebruiken om je gegevens te versleutelen", "pick_password_caution": "We slaan je wachtwoord niet op, dus als je het vergeet, zullen we u niet kunnen helpen uw data te herstellen zonder een herstelcode.", "key_generation_in_progress": "Encryptiesleutels genereren...", @@ -40,11 +40,12 @@ "referral_source_hint": "Hoe hoorde je over Ente? (optioneel)", "referral_source_info": "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!", "password_mismatch_error": "Wachtwoorden komen niet overeen", + "show_or_hide_password": "", "welcome_to_ente_title": "Welkom bij ", "welcome_to_ente_subtitle": "End-to-end versleutelde foto-opslag en uitwisseling", "new_album": "Nieuw album", "create_albums": "Albums aanmaken", - "enter_album_name": "Albumnaam", + "album_name": "Albumnaam", "close": "Sluiten", "yes": "Ja", "no": "Nee", @@ -58,11 +59,11 @@ "select_photos": "Selecteer foto's", "file_upload": "Bestand uploaden", "preparing": "Voorbereiden", - "processed_counts": "", - "upload_reading_metadata_files": "", + "processed_counts": "{{count, number}} / {{total, number}}", + "upload_reading_metadata_files": "Metadata bestanden lezen", "upload_cancelling": "Resterende uploads worden geannuleerd", - "upload_done": "", - "upload_skipped": "", + "upload_done": "{{count, number}} geüpload", + "upload_skipped": "{{count, number}} overgeslagen", "initial_load_delay_warning": "Eerste keer laden kan enige tijd duren", "no_account": "Heb nog geen account", "existing_account": "Heb al een account", @@ -95,15 +96,15 @@ "exit_fullscreen": "Volledig scherm verlaten", "go_fullscreen": "Naar volledig scherm", "zoom": "Zoom", - "play": "", - "pause": "", + "play": "Afspelen", + "pause": "Pauzeren", "previous": "Vorige", "next": "Volgende", - "video_seek": "", - "quality": "", - "auto": "", - "original": "", - "speed": "", + "video_seek": "Video zoeken", + "quality": "Kwaliteit", + "auto": "Automatisch", + "original": "Origineel", + "speed": "Snelheid", "title_photos": "Ente Foto's", "title_auth": "Ente Auth", "title_accounts": "Ente Accounts", @@ -625,6 +626,7 @@ "faster_upload_description": "Uploaden door nabije servers", "open_ente_on_startup": "Open Ente tijdens opstarten", "cast_album_to_tv": "Album afspelen op TV", + "cast_to_tv": "", "enter_cast_pin_code": "Voer de code in die u op de TV ziet om dit apparaat te koppelen.", "code": "Code", "pair_device_to_tv": "Koppel apparaten", @@ -675,5 +677,11 @@ "theme": "Thema", "system": "Systeem", "light": "Licht", - "dark": "Donker" + "dark": "Donker", + "streamable_videos": "Video's met stream", + "processing_videos_status": "Video's verwerken...", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/pl-PL/translation.json b/web/packages/base/locales/pl-PL/translation.json index 4ef32234a7..a081d13022 100644 --- a/web/packages/base/locales/pl-PL/translation.json +++ b/web/packages/base/locales/pl-PL/translation.json @@ -32,7 +32,7 @@ "set_password": "Ustaw hasło", "sign_in": "Zaloguj się", "incorrect_password": "Nieprawidłowe hasło", - "incorrect_password_or_no_account": "", + "incorrect_password_or_no_account": "Niepoprawne hasło lub adres e-mail nie został zarejestrowany", "pick_password_hint": "Wprowadź hasło, którego możemy użyć do zaszyfrowania Twoich danych", "pick_password_caution": "Nie przechowujemy Twojego hasła, więc jeśli je zapomnisz, nie będziemy w stanie Ci pomóc odzyskać Twoich danych bez klucza odzyskiwania.", "key_generation_in_progress": "Generowanie kluczy szyfrujących...", @@ -40,11 +40,12 @@ "referral_source_hint": "Jak usłyszałeś/aś o Ente? (opcjonalnie)", "referral_source_info": "Nie śledzimy instalacji aplikacji. Pomogłyby nam, gdybyś powiedział/a nam, gdzie nas znalazłeś/aś!", "password_mismatch_error": "Hasła nie pasują do siebie", + "show_or_hide_password": "Pokaż lub ukryj hasło", "welcome_to_ente_title": "Witamy w ", "welcome_to_ente_subtitle": "Zaszyfrowana metodą end-to-end pamięć na zdjęcia i udostępnianie", "new_album": "Nowy album", "create_albums": "Utwórz albumy", - "enter_album_name": "Nazwa albumu", + "album_name": "Nazwa albumu", "close": "Zamknij", "yes": "Tak", "no": "Nie", @@ -59,7 +60,7 @@ "file_upload": "Przesył plików", "preparing": "Przygotowywanie", "processed_counts": "", - "upload_reading_metadata_files": "", + "upload_reading_metadata_files": "Czytanie plików metadanych", "upload_cancelling": "Anulowanie pozostałych przesłań", "upload_done": "", "upload_skipped": "", @@ -74,26 +75,26 @@ "download_uncategorized": "Pobierz nieskategoryzowane", "download_hidden_items": "Pobierz ukryte elementy", "audio": "", - "more": "", + "more": "Więcej", "mouse_scroll": "Przewijanie kółkiem myszy", "pan": "", "pinch": "", - "drag": "", - "tap_inside_image": "", - "tap_outside_image": "", - "shortcuts": "", - "show_shortcuts": "", + "drag": "Przeciągnij", + "tap_inside_image": "Dotknij wewnątrz obrazu", + "tap_outside_image": "Dotknij na zewnątrz obrazu", + "shortcuts": "Skróty", + "show_shortcuts": "Pokaż skróty", "zoom_preset": "", "toggle_controls": "", "toggle_live": "", "toggle_audio": "", "toggle_favorite": "", "toggle_archive": "", - "view_info": "", + "view_info": "Zobacz informacje", "copy_as_png": "Kopiuj jako PNG", "toggle_fullscreen": "Przełącz tryb pełnoekranowy", - "exit_fullscreen": "", - "go_fullscreen": "", + "exit_fullscreen": "Zamknij tryb pełnoekranowy", + "go_fullscreen": "Przejdź do trybu pełnoekranowego", "zoom": "", "play": "", "pause": "", @@ -101,7 +102,7 @@ "next": "Następny", "video_seek": "", "quality": "", - "auto": "", + "auto": "Automatycznie", "original": "", "speed": "", "title_photos": "Zdjęcia Ente", @@ -147,9 +148,9 @@ "no_recovery_key_message": "Ze względu na charakter naszego protokołu szyfrowania end-to-end, Twoje dane nie mogą być odszyfrowane bez hasła lub klucza odzyskiwania", "no_two_factor_recovery_key_message": "Wyślij wiadomość e-mail na {{emailID}} z zarejestrowanego adresu e-mail", "contact_support": "Skontaktuj się z pomocą techniczną", - "help": "", - "ente_help": "", - "blog": "", + "help": "Pomoc", + "ente_help": "Pomoc Ente", + "blog": "Blog", "request_feature": "Zaproponuj funkcję", "support": "Wsparcie Techniczne", "cancel": "Anuluj", @@ -224,8 +225,8 @@ "delete_photos": "Usuń zdjęcia", "keep_photos": "Zachowaj zdjęcia", "share_album": "Udostępnij album", - "sharing_with_self": "", - "sharing_already_shared": "", + "sharing_with_self": "Nie możesz udostępnić samemu sobie", + "sharing_already_shared": "Już udostępniasz to {{email}}", "sharing_album_not_allowed": "Udostępnianie albumu nie jest dozwolone", "sharing_disabled_for_free_accounts": "Udostępnianie jest wyłączone dla darmowych kont", "sharing_user_does_not_exist": "", @@ -493,7 +494,7 @@ "stop_watching_folder_message": "Twoje istniejące pliki nie zostaną usunięte, ale Ente przestanie automatycznie aktualizować połączony album Ente przy zmianach w tym folderze.", "yes_stop": "Tak, zatrzymaj", "change_folder": "Zmień Folder", - "view_logs": "", + "view_logs": "Wyświetl logi", "view_logs_message": "", "weak_device_hint": "Przeglądarka, której używasz nie jest wystarczająco silna, aby zaszyfrować Twoje zdjęcia. Prosimy zalogować się do Ente na Twoim komputerze lub pobierz aplikacje mobilną/komputerową Ente.", "drag_and_drop_hint": "Lub przeciągnij i upuść do okna Ente", @@ -594,7 +595,7 @@ "video": "Wideo", "live_photo": "Live Photo", "live": "", - "edit_image": "", + "edit_image": "Edytuj zdjęcie", "photo_editor": "Edytor zdjęć", "confirm_editor_close": "Czy na pewno chcesz zamknąć edytor?", "confirm_editor_close_message": "Pobierz edytowany obraz lub zapisz kopię do Ente, aby utrzymać zmiany.", @@ -625,6 +626,7 @@ "faster_upload_description": "Kieruj przesłania przez pobliskie serwery", "open_ente_on_startup": "", "cast_album_to_tv": "Odtwórz album na telewizorze", + "cast_to_tv": "", "enter_cast_pin_code": "Wprowadź kod, który widzisz na telewizorze poniżej, aby sparować to urządzenie.", "code": "Kod", "pair_device_to_tv": "Sparuj urządzenia", @@ -672,8 +674,14 @@ "server_endpoint": "Punkt końcowy serwera", "more_information": "Więcej informacji", "save": "Zapisz", - "theme": "", - "system": "", + "theme": "Motyw", + "system": "Systemowy", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/pt-BR/translation.json b/web/packages/base/locales/pt-BR/translation.json index bc6c058b3a..1ef0830b71 100644 --- a/web/packages/base/locales/pt-BR/translation.json +++ b/web/packages/base/locales/pt-BR/translation.json @@ -32,7 +32,7 @@ "set_password": "Definir senha", "sign_in": "Entrar", "incorrect_password": "Senha incorreta", - "incorrect_password_or_no_account": "", + "incorrect_password_or_no_account": "Senha incorreta ou e-mail não registrado", "pick_password_hint": "Insira uma senha que podemos usar para criptografar seus dados", "pick_password_caution": "Não armazenamos sua senha, portanto, caso se esqueça, não poderemos ajudar vocêa recuperar seus dados sem uma chave de recuperação.", "key_generation_in_progress": "Gerando chaves de criptografia...", @@ -40,11 +40,12 @@ "referral_source_hint": "Como você descobriu o Ente? (opcional)", "referral_source_info": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!", "password_mismatch_error": "As senhas não correspondem", + "show_or_hide_password": "Exibir ou ocultar senha", "welcome_to_ente_title": "Bem-vindo à ", "welcome_to_ente_subtitle": "Armazenamento de fotos e compartilhamento criptografado de ponta a ponta", "new_album": "Novo álbum", "create_albums": "Criar álbuns", - "enter_album_name": "Nome do álbum", + "album_name": "Nome do álbum", "close": "Fechar", "yes": "Sim", "no": "Não", @@ -625,6 +626,7 @@ "faster_upload_description": "Rotas enviam em servidores próximos", "open_ente_on_startup": "Abrir Ente no Início", "cast_album_to_tv": "Reproduzir álbum na TV", + "cast_to_tv": "", "enter_cast_pin_code": "Digite o código que você vê na TV abaixo para parear este dispositivo.", "code": "Código", "pair_device_to_tv": "Parear dispositivos", @@ -675,5 +677,11 @@ "theme": "Tema", "system": "Automático", "light": "Claro", - "dark": "Escuro" + "dark": "Escuro", + "streamable_videos": "Vídeos transmissíveis", + "processing_videos_status": "Processando vídeos...", + "share_favorites": "Compartilhar favoritos", + "person_favorites": "Favoritos de {{name}}", + "shared_favorites": "Favoritos compartilhados", + "added_by_name": "Adicionado por {{name}}" } diff --git a/web/packages/base/locales/pt-PT/translation.json b/web/packages/base/locales/pt-PT/translation.json index 943ca807af..33e1c25e89 100644 --- a/web/packages/base/locales/pt-PT/translation.json +++ b/web/packages/base/locales/pt-PT/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "Como é que soube do Ente? (opcional)", "referral_source_info": "Não monitorizamos as instalações de aplicações. Seria útil se nos dissesse onde nos encontrou!", "password_mismatch_error": "As palavras-passe não correspondem", + "show_or_hide_password": "", "welcome_to_ente_title": "Bem-vindo ao ", "welcome_to_ente_subtitle": "Armazenamento criptografado de ponta a ponta de fotos e compartilhamento", "new_album": "Novo álbum", "create_albums": "Criar álbuns", - "enter_album_name": "Nome do álbum", + "album_name": "Nome do álbum", "close": "Fechar", "yes": "Sim", "no": "Não", @@ -625,6 +626,7 @@ "faster_upload_description": "Rotas enviam em servidores próximos", "open_ente_on_startup": "", "cast_album_to_tv": "Reproduzir álbum na TV", + "cast_to_tv": "", "enter_cast_pin_code": "Introduza o código que vê na TV abaixo para emparelhar este dispositivo.", "code": "", "pair_device_to_tv": "Emparelhar dispositivos", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/ro-RO/translation.json b/web/packages/base/locales/ro-RO/translation.json index a1f96136ef..f2a3bd8d57 100644 --- a/web/packages/base/locales/ro-RO/translation.json +++ b/web/packages/base/locales/ro-RO/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/ru-RU/translation.json b/web/packages/base/locales/ru-RU/translation.json index dccb85856d..dd0c219502 100644 --- a/web/packages/base/locales/ru-RU/translation.json +++ b/web/packages/base/locales/ru-RU/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "Как вы узнали о Ente? (необязательно)", "referral_source_info": "Будет полезно, если вы укажете, где вы узнали о нас, так как мы не отслеживаем установки приложения!", "password_mismatch_error": "Пароли не совпадают", + "show_or_hide_password": "", "welcome_to_ente_title": "Добро пожаловать в ", "welcome_to_ente_subtitle": "Сквозное зашифрованное хранение фотографий и общий доступ к ним", "new_album": "Новый альбом", "create_albums": "Создать альбомы", - "enter_album_name": "Название альбома", + "album_name": "Название альбома", "close": "Закрыть", "yes": "Да", "no": "Нет", @@ -625,6 +626,7 @@ "faster_upload_description": "Загрузка маршрута через близлежащие серверы", "open_ente_on_startup": "", "cast_album_to_tv": "Воспроизвести альбом на ТВ", + "cast_to_tv": "", "enter_cast_pin_code": "Введите код, который вы видите на экране телевизора ниже, чтобы выполнить сопряжение с этим устройством.", "code": "Код", "pair_device_to_tv": "Сопряжение устройств", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/sl-SI/translation.json b/web/packages/base/locales/sl-SI/translation.json index a1f96136ef..f2a3bd8d57 100644 --- a/web/packages/base/locales/sl-SI/translation.json +++ b/web/packages/base/locales/sl-SI/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/sr-SP/translation.json b/web/packages/base/locales/sr-SP/translation.json new file mode 100644 index 0000000000..f2a3bd8d57 --- /dev/null +++ b/web/packages/base/locales/sr-SP/translation.json @@ -0,0 +1,687 @@ +{ + "intro_slide_1_title": "", + "intro_slide_1": "", + "intro_slide_2_title": "", + "intro_slide_2": "", + "intro_slide_3_title": "", + "intro_slide_3": "", + "login": "", + "sign_up": "", + "new_to_ente": "", + "existing_user": "", + "enter_email": "", + "invalid_email_error": "", + "required": "", + "email_not_registered": "", + "email_already_registered": "", + "email_sent": "", + "check_inbox_hint": "", + "verification_code": "", + "resend_code": "", + "verify": "", + "send_otp": "", + "generic_error": "", + "generic_error_retry": "", + "invalid_code_error": "", + "expired_code_error": "", + "status_sending": "", + "status_sent": "", + "password": "", + "link_password_description": "", + "unlock": "", + "set_password": "", + "sign_in": "", + "incorrect_password": "", + "incorrect_password_or_no_account": "", + "pick_password_hint": "", + "pick_password_caution": "", + "key_generation_in_progress": "", + "confirm_password": "", + "referral_source_hint": "", + "referral_source_info": "", + "password_mismatch_error": "", + "show_or_hide_password": "", + "welcome_to_ente_title": "", + "welcome_to_ente_subtitle": "", + "new_album": "", + "create_albums": "", + "album_name": "", + "close": "", + "yes": "", + "no": "", + "nothing_here": "", + "upload": "", + "import": "", + "add_photos": "", + "add_more_photos": "", + "add_photos_count_one": "", + "add_photos_count": "", + "select_photos": "", + "file_upload": "", + "preparing": "", + "processed_counts": "", + "upload_reading_metadata_files": "", + "upload_cancelling": "", + "upload_done": "", + "upload_skipped": "", + "initial_load_delay_warning": "", + "no_account": "", + "existing_account": "", + "create": "", + "files_count": "", + "download": "", + "download_album": "", + "download_favorites": "", + "download_uncategorized": "", + "download_hidden_items": "", + "audio": "", + "more": "", + "mouse_scroll": "", + "pan": "", + "pinch": "", + "drag": "", + "tap_inside_image": "", + "tap_outside_image": "", + "shortcuts": "", + "show_shortcuts": "", + "zoom_preset": "", + "toggle_controls": "", + "toggle_live": "", + "toggle_audio": "", + "toggle_favorite": "", + "toggle_archive": "", + "view_info": "", + "copy_as_png": "", + "toggle_fullscreen": "", + "exit_fullscreen": "", + "go_fullscreen": "", + "zoom": "", + "play": "", + "pause": "", + "previous": "", + "next": "", + "video_seek": "", + "quality": "", + "auto": "", + "original": "", + "speed": "", + "title_photos": "", + "title_auth": "", + "title_accounts": "", + "upload_first_photo": "", + "import_your_folders": "", + "upload_dropzone_hint": "", + "watch_folder_dropzone_hint": "", + "trash_files_title": "", + "trash_file_title": "", + "delete_files_title": "", + "delete_files_message": "", + "selected_count": "", + "selected_and_yours_count": "", + "delete": "", + "favorite": "", + "convert": "", + "multi_folder_upload": "", + "upload_to_choice": "", + "upload_to_single_album": "", + "upload_to_album_per_folder": "", + "session_expired": "", + "session_expired_message": "", + "password_generation_failed": "", + "change_password": "", + "password_changed_elsewhere": "", + "password_changed_elsewhere_message": "", + "go_back": "", + "account": "", + "recovery_key": "", + "do_this_later": "", + "save_key": "", + "recovery_key_description": "", + "key_not_stored_note": "", + "recovery_key_generation_failed": "", + "forgot_password": "", + "recover_account": "", + "recover": "", + "no_recovery_key_title": "", + "incorrect_recovery_key": "", + "sorry": "", + "no_recovery_key_message": "", + "no_two_factor_recovery_key_message": "", + "contact_support": "", + "help": "", + "ente_help": "", + "blog": "", + "request_feature": "", + "support": "", + "cancel": "", + "logout": "", + "logout_message": "", + "delete_account": "", + "delete_account_manually_message": "", + "change_email": "", + "ok": "", + "success": "", + "error": "", + "offline_message": "", + "install": "", + "install_mobile_app": "", + "download_app": "", + "download_app_message": "", + "subscription": "", + "manage_payment_method": "", + "manage_family": "", + "family_plan": "", + "leave_family_plan": "", + "leave": "", + "leave_family_plan_confirm": "", + "choose_plan": "", + "manage_plan": "", + "current_usage": "", + "two_months_free": "", + "free_plan_option": "", + "free_plan_description": "", + "active": "", + "subscription_info_free": "", + "subscription_info_family": "", + "subscription_info_expired": "", + "subscription_info_renewal_cancelled": "", + "subscription_info_storage_quota_exceeded": "", + "subscription_status_renewal_active": "", + "subscription_status_renewal_cancelled": "", + "add_on_valid_till": "", + "subscription_expired": "", + "storage_quota_exceeded": "", + "subscription_purchase_success": "", + "subscription_purchase_cancelled": "", + "subscription_purchase_failed": "", + "subscription_verification_error": "", + "update_payment_method_message": "", + "payment_method_authentication_failed": "", + "update_payment_method": "", + "monthly": "", + "yearly": "", + "month_short": "", + "year": "", + "update_subscription": "", + "update_subscription_title": "", + "update_subscription_message": "", + "cancel_subscription": "", + "cancel_subscription_message": "", + "cancel_subscription_with_addon_message": "", + "subscription_cancel_success": "", + "reactivate_subscription": "", + "reactivate_subscription_message": "", + "subscription_activate_success": "", + "thank_you": "", + "cancel_subscription_on_mobile": "", + "cancel_subscription_on_mobile_message": "", + "mail_to_manage_subscription": "", + "rename": "", + "rename_file": "", + "rename_album": "", + "delete_album": "", + "delete_album_title": "", + "delete_album_message": "", + "delete_photos": "", + "keep_photos": "", + "share_album": "", + "sharing_with_self": "", + "sharing_already_shared": "", + "sharing_album_not_allowed": "", + "sharing_disabled_for_free_accounts": "", + "sharing_user_does_not_exist": "", + "search": "", + "search_results": "", + "no_results": "", + "search_hint": "", + "album": "", + "date": "", + "description": "", + "file_type": "", + "magic": "", + "photos_count_zero": "", + "photos_count_one": "", + "photos_count": "", + "terms_and_conditions": "", + "people": "", + "indexing_scheduled": "", + "indexing_photos": "", + "indexing_fetching": "", + "indexing_people": "", + "syncing_wait": "", + "people_empty_too_few": "", + "unnamed_person": "", + "add_a_name": "", + "new_person": "", + "add_name": "", + "rename_person": "", + "reset_person_confirm": "", + "reset_person_confirm_message": "", + "ignore": "", + "ignore_person_confirm": "", + "ignore_person_confirm_message": "", + "ignored": "", + "show_person": "", + "review_suggestions": "", + "saved_choices": "", + "discard_changes": "", + "discard_changes_confirm_message": "", + "people_suggestions_finding": "", + "people_suggestions_empty": "", + "info": "", + "file_name": "", + "caption_placeholder": "", + "location": "", + "view_on_map": "", + "map": "", + "enable_map": "", + "enable_maps_confirm": "", + "enable_maps_confirm_message": "", + "disable_map": "", + "disable_maps_confirm": "", + "disable_maps_confirm_message": "", + "details": "", + "view_exif": "", + "no_exif": "", + "exif": "", + "two_factor": "", + "two_factor_authentication": "", + "two_factor_qr_help": "", + "two_factor_manual_entry_title": "", + "two_factor_manual_entry_message": "", + "scan_qr_title": "", + "enable_two_factor": "", + "enable": "", + "enabled": "", + "lost_2fa_device": "", + "incorrect_code": "", + "two_factor_info": "", + "disable": "", + "reconfigure": "", + "reconfigure_two_factor_hint": "", + "update_two_factor": "", + "update_two_factor_message": "", + "update": "", + "disable_two_factor": "", + "disable_two_factor_message": "", + "export_data": "", + "select_folder": "", + "select_zips": "", + "faq": "", + "takeout_hint": "", + "destination": "", + "start": "", + "last_export_time": "", + "export_again": "", + "local_storage_not_accessible": "", + "email_already_taken": "", + "live_photos_detected": "", + "ignored_uploads": "", + "ignored_uploads_hint": "", + "file_not_uploaded_list": "", + "failed_uploads": "", + "failed_uploads_hint": "", + "retry_failed_uploads": "", + "thumbnail_generation_failed": "", + "thumbnail_generation_failed_hint": "", + "unsupported_files": "", + "unsupported_files_hint": "", + "blocked_uploads": "", + "blocked_uploads_hint": "", + "large_files": "", + "large_files_hint": "", + "insufficient_storage": "", + "insufficient_storage_hint": "", + "uploads_in_progress": "", + "successful_uploads": "", + "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_album": "", + "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": "", + "confirm_remove_message": "", + "confirm_remove_incl_others_message": "", + "oldest": "", + "last_updated": "", + "name": "", + "fix_creation_time": "", + "fix_creation_time_in_progress": "", + "fix_creation_time_file_updated": "", + "fix_creation_time_completed": "", + "fix_creation_time_completed_with_errors": "", + "fix_creation_time_options": "", + "exif_date_time_original": "", + "exif_date_time_digitized": "", + "exif_metadata_date": "", + "custom_time": "", + "caption_character_limit": "", + "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_permission_to_viewer": "", + "change_permission_to_collaborator": "", + "change_permission_title": "", + "confirm_convert_to_viewer": "", + "confirm_convert_to_collaborator": "", + "manage": "", + "added_as": "", + "collaborator_hint": "", + "remove_participant": "", + "remove_participant_title": "", + "remove_participant_message": "", + "confirm_remove": "", + "owner": "", + "collaborators": "", + "viewers": "", + "add_more": "", + "or_add_existing": "", + "not_found": "", + "link_expired": "", + "link_expired_message": "", + "manage_link": "", + "link_request_limit_exceeded": "", + "allow_downloads": "", + "allow_adding_photos": "", + "allow_adding_photos_hint": "", + "device_limit": "", + "none": "", + "link_expiry": "", + "never": "", + "after_time": { + "hour": "", + "day": "", + "week": "", + "month": "", + "year": "" + }, + "copy_link": "", + "done": "", + "share_link_section_title": "", + "remove_link": "", + "create_public_link": "", + "public_link_created": "", + "public_link_enabled": "", + "collect_photos": "", + "disable_file_download": "", + "disable_file_download_message": "", + "shared_using": "", + "sharing_referral_code": "", + "disable_password": "", + "disable_password_message": "", + "password_lock": "", + "lock": "", + "file": "", + "folder": "", + "google_takeout": "", + "deduplicate_files": "", + "remove_duplicates": "", + "total_size": "", + "count": "", + "deselect_all": "", + "no_duplicates": "", + "duplicate_group_description": "", + "remove_duplicates_button_count": "", + "stop_uploads_title": "", + "stop_uploads_message": "", + "yes_stop_uploads": "", + "stop_downloads_title": "", + "stop_downloads_message": "", + "yes_stop_downloads": "", + "albums": "", + "albums_count_one": "", + "albums_count": "", + "all_albums": "", + "all_hidden_albums": "", + "hidden_albums": "", + "hidden_items": "", + "enter_two_factor_otp": "", + "create_account": "", + "copied": "", + "upgrade_now": "", + "renew_now": "", + "storage": "", + "used": "", + "you": "", + "family": "", + "free": "", + "of": "", + "watch_folders": "", + "watched_folders": "", + "no_folders_added": "", + "watch_folders_hint_1": "", + "watch_folders_hint_2": "", + "watch_folders_hint_3": "", + "add_folder": "", + "stop_watching": "", + "stop_watching_folder_title": "", + "stop_watching_folder_message": "", + "yes_stop": "", + "change_folder": "", + "view_logs": "", + "view_logs_message": "", + "weak_device_hint": "", + "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": "", + "enter_name": "", + "uploader_name_hint": "", + "name_placeholder": "", + "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": "", + "passphrase_strength_weak": "", + "passphrase_strength_moderate": "", + "passphrase_strength_strong": "", + "preferences": "", + "language": "", + "advanced": "", + "export_directory_does_not_exist": "", + "export_directory_does_not_exist_message": "", + "storage_unit": { + "b": "", + "kb": "", + "mb": "", + "gb": "", + "tb": "" + }, + "stop": "", + "sync_continuously": "", + "export_starting": "", + "export_preparing": "", + "export_renaming_album_folders": "", + "export_trashing_deleted_files": "", + "export_trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", + "delete_account_reason_label": "", + "delete_account_reason_placeholder": "", + "delete_reason": { + "missing_feature": "", + "behaviour": "", + "found_another_service": "", + "not_listed": "" + }, + "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": "", + "no_codes_added_yet": "", + "hide": "", + "unhide": "", + "sort_by": "", + "newest_first": "", + "oldest_first": "", + "pin_album": "", + "unpin_album": "", + "unpreviewable_file_message": "", + "download_complete": "", + "downloading_album": "", + "download_failed": "", + "download_progress": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", + "image": "", + "video": "", + "live_photo": "", + "live": "", + "edit_image": "", + "photo_editor": "", + "confirm_editor_close": "", + "confirm_editor_close_message": "", + "brightness": "", + "contrast": "", + "saturation": "", + "blur": "", + "transform": "", + "crop": "", + "aspect_ratio": "", + "square": "", + "freehand": "", + "apply_crop": "", + "rotation": "", + "rotate_left": "", + "rotate_right": "", + "flip": "", + "flip_vertically": "", + "flip_horizontally": "", + "download_edited": "", + "save_a_copy_to_ente": "", + "restore_original": "", + "photo_edit_required_to_save": "", + "colors": "", + "invert_colors": "", + "reset": "", + "faster_upload": "", + "faster_upload_description": "", + "open_ente_on_startup": "", + "cast_album_to_tv": "", + "cast_to_tv": "", + "enter_cast_pin_code": "", + "code": "", + "pair_device_to_tv": "", + "tv_not_found": "", + "cast_auto_pair": "", + "cast_auto_pair_description": "", + "choose_device_from_browser": "", + "cast_auto_pair_failed": "", + "pair_with_pin": "", + "pair_with_pin_description": "", + "visit_cast_url": "", + "passkeys": "", + "passkey_fetch_failed": "", + "manage_passkey": "", + "delete_passkey": "", + "delete_passkey_confirmation": "", + "rename_passkey": "", + "add_passkey": "", + "enter_passkey_name": "", + "passkeys_description": "", + "created_at": "", + "passkey_add_failed": "", + "passkey_login_failed": "", + "passkey_login_invalid_url": "", + "passkey_login_already_claimed_session": "", + "passkey_login_generic_error": "", + "passkey_login_credential_hint": "", + "passkeys_not_supported": "", + "try_again": "", + "check_status": "", + "passkey_login_instructions": "", + "passkey_login": "", + "totp_login": "", + "passkey": "", + "passkey_verify_description": "", + "waiting_for_verification": "", + "verification_still_pending": "", + "passkey_verified": "", + "redirecting_back_to_app": "", + "redirect_close_instructions": "", + "redirect_again": "", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "", + "theme": "", + "system": "", + "light": "", + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" +} diff --git a/web/packages/base/locales/sv-SE/translation.json b/web/packages/base/locales/sv-SE/translation.json index 9f0de8a9f8..1f855f2081 100644 --- a/web/packages/base/locales/sv-SE/translation.json +++ b/web/packages/base/locales/sv-SE/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "Hur hörde du talas om Ente? (valfritt)", "referral_source_info": "Vi spårar inte appinstallationer, Det skulle hjälpa oss om du berättade var du hittade oss!", "password_mismatch_error": "Lösenorden matchar inte", + "show_or_hide_password": "", "welcome_to_ente_title": "Välkommen till ", "welcome_to_ente_subtitle": "Totalsträckskrypterad lagring och delning av foton", "new_album": "Nytt album", "create_albums": "Skapa album", - "enter_album_name": "Albumnamn", + "album_name": "Albumnamn", "close": "Stäng", "yes": "Ja", "no": "Nej", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "Spela album på TV", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/ta-IN/translation.json b/web/packages/base/locales/ta-IN/translation.json index a1f96136ef..f2a3bd8d57 100644 --- a/web/packages/base/locales/ta-IN/translation.json +++ b/web/packages/base/locales/ta-IN/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/te-IN/translation.json b/web/packages/base/locales/te-IN/translation.json index a1f96136ef..f2a3bd8d57 100644 --- a/web/packages/base/locales/te-IN/translation.json +++ b/web/packages/base/locales/te-IN/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/th-TH/translation.json b/web/packages/base/locales/th-TH/translation.json index a1f96136ef..f2a3bd8d57 100644 --- a/web/packages/base/locales/th-TH/translation.json +++ b/web/packages/base/locales/th-TH/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/ti-ER/translation.json b/web/packages/base/locales/ti-ER/translation.json index a1f96136ef..f2a3bd8d57 100644 --- a/web/packages/base/locales/ti-ER/translation.json +++ b/web/packages/base/locales/ti-ER/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/tr-TR/translation.json b/web/packages/base/locales/tr-TR/translation.json index dcdf164520..1d59f0a6d6 100644 --- a/web/packages/base/locales/tr-TR/translation.json +++ b/web/packages/base/locales/tr-TR/translation.json @@ -32,7 +32,7 @@ "set_password": "Parola ayarla", "sign_in": "Giriş yap", "incorrect_password": "Yanlış parola", - "incorrect_password_or_no_account": "", + "incorrect_password_or_no_account": "Yanlış şifre veya e-posta kayıtlı değil", "pick_password_hint": "Lütfen verilerini şifrelemek için kullanabileceğimiz bir parola gir", "pick_password_caution": "Parolanı saklamıyoruz, dolayısıyla unutursan kurtarma anahtarın olmadan verilerini kurtarmana yardım edemeyiz.", "key_generation_in_progress": "Şifreleme anahtarları oluşturuluyor...", @@ -40,11 +40,12 @@ "referral_source_hint": "Ente'yi nereden duydun? (tercihe bağlı)", "referral_source_info": "Biz uygulama kurulumlarını takip etmiyoruz. Bizi nereden duyduğundan bahsetmen bize çok yardımcı olacak!", "password_mismatch_error": "Parolalar eşleşmiyor", + "show_or_hide_password": "Şifreyi göster veya gizle", "welcome_to_ente_title": "'ye hoş geldin", "welcome_to_ente_subtitle": "Uçtan uca şifreli fotoğraf depolama ve paylaşımı", "new_album": "Yeni albüm", "create_albums": "Albüm oluştur", - "enter_album_name": "Albüm adı", + "album_name": "Albüm adı", "close": "Kapat", "yes": "Evet", "no": "Hayır", @@ -625,6 +626,7 @@ "faster_upload_description": "Yüklemeleri yakındaki sunucular üzerinden yönlendir", "open_ente_on_startup": "Başlangıçta Ente’yi aç", "cast_album_to_tv": "Albümü TV'de oynat", + "cast_to_tv": "", "enter_cast_pin_code": "Bu cihazı eşleştirmek için TV ekranında gördüğünüz kodu girin.", "code": "Kod", "pair_device_to_tv": "Cihazları eşleştir", @@ -675,5 +677,11 @@ "theme": "Tema", "system": "Sistem", "light": "Aydınlık", - "dark": "Karanlık" + "dark": "Karanlık", + "streamable_videos": "Yayınlanabilir videolar", + "processing_videos_status": "Videolar işleniyor...", + "share_favorites": "Favorileri paylaş", + "person_favorites": "{{name}}'in favorileri", + "shared_favorites": "Paylaşılan favoriler", + "added_by_name": "{{name}} tarafından eklendi" } diff --git a/web/packages/base/locales/uk-UA/translation.json b/web/packages/base/locales/uk-UA/translation.json index eb51292d9c..db6b958869 100644 --- a/web/packages/base/locales/uk-UA/translation.json +++ b/web/packages/base/locales/uk-UA/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "Як ви дізналися про Ente? (необов'язково)", "referral_source_info": "Ми не відстежуємо встановлення застосунку. Але, якщо ви скажете нам, де ви нас знайшли, це допоможе!", "password_mismatch_error": "Паролі не співпадають", + "show_or_hide_password": "", "welcome_to_ente_title": "Вітаємо в ", "welcome_to_ente_subtitle": "Наскрізне зашифроване зберігання та спільний доступ до фотографій", "new_album": "Новий альбом", "create_albums": "Створити альбоми", - "enter_album_name": "Назва альбому", + "album_name": "Назва альбому", "close": "Закрити", "yes": "Так", "no": "Ні", @@ -625,6 +626,7 @@ "faster_upload_description": "Завантаження маршрутів через найближчі сервери", "open_ente_on_startup": "", "cast_album_to_tv": "Відтворення альбому на ТБ", + "cast_to_tv": "", "enter_cast_pin_code": "Введіть код, який ви бачите на телевізорі нижче, щоб з'єднатися з цим пристроєм.", "code": "Код", "pair_device_to_tv": "З'єднання між пристроями", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/vi-VN/translation.json b/web/packages/base/locales/vi-VN/translation.json index 2b8c3dabf6..879a2946ee 100644 --- a/web/packages/base/locales/vi-VN/translation.json +++ b/web/packages/base/locales/vi-VN/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "Bạn đã nghe về Ente từ đâu? (tùy chọn)", "referral_source_info": "Chúng tôi không theo dõi cài đặt ứng dụng, sẽ rất hữu ích nếu bạn cho chúng tôi biết bạn đã tìm thấy chúng tôi ở đâu!", "password_mismatch_error": "Mật khẩu không khớp", + "show_or_hide_password": "", "welcome_to_ente_title": "Chào mừng đến với ", "welcome_to_ente_subtitle": "Lưu trữ và chia sẻ ảnh được mã hóa đầu cuối", "new_album": "Album mới", "create_albums": "Tạo album", - "enter_album_name": "Tên album", + "album_name": "Tên album", "close": "Đóng", "yes": "Có", "no": "Không", @@ -625,6 +626,7 @@ "faster_upload_description": "Định tuyến tải lên qua các máy chủ gần đó", "open_ente_on_startup": "", "cast_album_to_tv": "Phát album trên TV", + "cast_to_tv": "", "enter_cast_pin_code": "Nhập mã bạn thấy trên TV bên dưới để ghép nối thiết bị này.", "code": "Mã", "pair_device_to_tv": "Ghép nối thiết bị", @@ -675,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/zh-CN/translation.json b/web/packages/base/locales/zh-CN/translation.json index a8a3050147..783517c04a 100644 --- a/web/packages/base/locales/zh-CN/translation.json +++ b/web/packages/base/locales/zh-CN/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "您是如何知道Ente的? (可选的)", "referral_source_info": "我们不跟踪应用程序安装情况,如果您告诉我们您是在哪里找到我们的,将会有所帮助!", "password_mismatch_error": "两次输入的密码不一致", + "show_or_hide_password": "", "welcome_to_ente_title": "欢迎来到 ", "welcome_to_ente_subtitle": "端到端加密的照片存储和共享", "new_album": "新建相册", "create_albums": "创建相册", - "enter_album_name": "相册名称", + "album_name": "相册名称", "close": "关闭", "yes": "是", "no": "否", @@ -625,6 +626,7 @@ "faster_upload_description": "通过附近的服务器路由上传", "open_ente_on_startup": "启动时打开 Ente", "cast_album_to_tv": "在电视上播放相册", + "cast_to_tv": "", "enter_cast_pin_code": "输入您在下面的电视上看到的代码来配对此设备。", "code": "代码", "pair_device_to_tv": "配对设备", @@ -675,5 +677,11 @@ "theme": "主题", "system": "系统", "light": "浅色", - "dark": "深色" + "dark": "深色", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/zh-HK/translation.json b/web/packages/base/locales/zh-HK/translation.json index 0d37270276..25d9800737 100644 --- a/web/packages/base/locales/zh-HK/translation.json +++ b/web/packages/base/locales/zh-HK/translation.json @@ -40,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "歡迎來到 ", "welcome_to_ente_subtitle": "", "new_album": "新相簿", "create_albums": "創建相簿", - "enter_album_name": "相簿名稱", + "album_name": "相簿名稱", "close": "關閉", "yes": "是", "no": "否", @@ -625,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "配對設備", @@ -675,5 +677,11 @@ "theme": "主題", "system": "系統", "light": "亮色", - "dark": "暗色" + "dark": "暗色", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/next.config.base.js b/web/packages/base/next.config.base.js index 5bb855f792..8afb699564 100644 --- a/web/packages/base/next.config.base.js +++ b/web/packages/base/next.config.base.js @@ -64,7 +64,7 @@ const isDesktop = process.env._ENTE_IS_DESKTOP ? "1" : ""; /** * When we're running within the desktop app, also extract the version of the - * desktop app for use in our "X-Client-Package" string. + * desktop app for use in our "X-Client-Version" string. * * > The web app has continuous deployments, and doesn't have versions. */ diff --git a/web/packages/base/package.json b/web/packages/base/package.json index 9278992a43..73a0db5e57 100644 --- a/web/packages/base/package.json +++ b/web/packages/base/package.json @@ -11,23 +11,23 @@ "comlink": "^4.4.2", "ente-utils": "*", "formik": "^2.4.6", - "get-user-locale": "^2.3.2", - "i18next": "^24.2.3", + "get-user-locale": "^3.0.0", + "i18next": "^25.2.0", "i18next-resources-to-backend": "^1.2.1", - "idb": "^8.0.2", + "idb": "^8.0.3", "libsodium-wrappers-sumo": "^0.7.15", "nanoid": "^5.1.5", - "next": "^15.3.1", + "next": "^15.3.2", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-i18next": "^15.4.1", + "react-i18next": "^15.5.2", "yup": "^1.6.1", - "zod": "^3.24.3" + "zod": "^3.25.23" }, "devDependencies": { "@types/libsodium-wrappers-sumo": "^0.7.8", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.3", + "@types/react": "^19.1.5", + "@types/react-dom": "^19.1.5", "ente-build-config": "*" } } diff --git a/web/packages/base/session.ts b/web/packages/base/session.ts index b416b8ed9a..5b523b64a3 100644 --- a/web/packages/base/session.ts +++ b/web/packages/base/session.ts @@ -2,6 +2,47 @@ import { z } from "zod"; import { decryptBox } from "./crypto"; import { toB64 } from "./crypto/libsodium"; +/** + * Remove all data stored in session storage (data tied to the browser tab). + * + * See `docs/storage.md` for more details about session storage. Currently, only + * the following entries are stored in session storage: + * + * - "encryptionKey" + * - "keyEncryptionKey" (transient) + */ +export const clearSessionStorage = () => sessionStorage.clear(); + +/** + * Schema of JSON string value for the "encryptionKey" and "keyEncryptionKey" + * keys strings stored in session storage. + */ +const SessionKeyData = z.object({ + encryptedData: z.string(), + key: z.string(), + nonce: z.string(), +}); + +/** + * Save the user's encrypted master key in the session storage. + * + * @param keyB64 The user's master key as a base64 encoded string. + */ +// TODO(RE): +// export const saveMasterKeyInSessionStore = async ( +// keyB64: string, +// fromDesktop?: boolean, +// ) => { +// const cryptoWorker = await sharedCryptoWorker(); +// const sessionKeyAttributes = +// await cryptoWorker.generateKeyAndEncryptToB64(key); +// setKey(keyType, sessionKeyAttributes); +// const electron = globalThis.electron; +// if (electron && !fromDesktop) { +// electron.saveMasterKeyB64(key); +// } +// }; + /** * Return the user's decrypted master key from session storage. * @@ -37,7 +78,7 @@ export const masterKeyFromSessionIfLoggedIn = async () => { const value = sessionStorage.getItem("encryptionKey"); if (!value) return undefined; - const { encryptedData, key, nonce } = EncryptionKeyAttributes.parse( + const { encryptedData, key, nonce } = SessionKeyData.parse( JSON.parse(value), ); return decryptBox({ encryptedData, nonce }, key); @@ -49,9 +90,31 @@ export const masterKeyFromSessionIfLoggedIn = async () => { */ export const masterKeyB64FromSession = () => masterKeyFromSession().then(toB64); -// TODO: Same as B64EncryptionResult. Revisit. -const EncryptionKeyAttributes = z.object({ - encryptedData: z.string(), - key: z.string(), - nonce: z.string(), -}); +/** + * Return the decrypted user's key encryption key ("kek") from session storage + * if present, otherwise return `undefined`. + * + * [Note: Stashing kek in session store] + * + * During login, if the user has set a second factor (passkey or TOTP), then we + * need to redirect them to the accounts app or TOTP page to verify the second + * factor. This second factor verification happens after password verification, + * but simply storing the decrypted kek in-memory wouldn't work because the + * second factor redirect can happen to a separate accounts app altogether. + * + * So instead, we stash the encrypted kek in session store (using + * {@link stashKeyEncryptionKeyInSessionStore}), and after redirect, retrieve + * it (after clearing it) using {@link unstashKeyEncryptionKeyFromSession}. + */ +export const unstashKeyEncryptionKeyFromSession = async () => { + // TODO: Same value as the deprecated getKey("keyEncryptionKey") + const value = sessionStorage.getItem("keyEncryptionKey"); + if (!value) return undefined; + + sessionStorage.removeItem("keyEncryptionKey"); + + const { encryptedData, key, nonce } = SessionKeyData.parse( + JSON.parse(value), + ); + return decryptBox({ encryptedData, nonce }, key); +}; diff --git a/web/packages/base/types/ipc.ts b/web/packages/base/types/ipc.ts index 083cc81037..d44a92e980 100644 --- a/web/packages/base/types/ipc.ts +++ b/web/packages/base/types/ipc.ts @@ -316,10 +316,10 @@ export interface Electron { * The behaviour is OS dependent. On macOS we use the `sips` utility, while * on Linux and Windows we use a `vips` bundled with our desktop app. * - * @param dataOrPathOrZipItem The file whose thumbnail we want to generate. - * It can be provided as raw image data (the contents of the image file), or - * the path to the image file, or a tuple containing the path of the zip - * file along with the name of an entry in it. + * @param pathOrZipItem The file whose thumbnail we want to generate. It can + * be provided as raw image data (the contents of the image file), or the + * path to the image file, or a tuple containing the path of the zip file + * along with the name of an entry in it. * * @param maxDimension The maximum width or height of the generated * thumbnail. @@ -329,14 +329,13 @@ export interface Electron { * @returns JPEG data of the generated thumbnail. */ generateImageThumbnail: ( - dataOrPathOrZipItem: Uint8Array | string | ZipItem, + pathOrZipItem: string | ZipItem, maxDimension: number, maxSize: number, ) => Promise; /** - * Execute a FFmpeg {@link command} on the given - * {@link dataOrPathOrZipItem}. + * Execute a FFmpeg {@link command} on the given {@link pathOrZipItem}. * * This executes the command using a FFmpeg executable we bundle with our * desktop app. We also have a Wasm FFmpeg implementation that we use when @@ -349,11 +348,11 @@ export interface Electron { * (respectively {@link inputPathPlaceholder}, * {@link outputPathPlaceholder}, {@link ffmpegPathPlaceholder}). * - * @param dataOrPathOrZipItem The bytes of the input file, or the path to - * the input file on the user's local disk, or the path to a zip file on the - * user's disk and the name of an entry in it. In all three cases, the data - * gets serialized to a temporary file, and then that path gets substituted - * in the FFmpeg {@link command} in lieu of {@link inputPathPlaceholder}. + * @param pathOrZipItem The path to the input file on the user's local disk, + * or the path to a zip file on the user's disk and the name of an entry in + * it. In the second case, the data gets serialized to a temporary file, and + * then that path (or if it was already a path) gets substituted in the + * FFmpeg {@link command} in lieu of {@link inputPathPlaceholder}. * * @param outputFileExtension The extension (without the dot, e.g. "jpeg") * to use for the output file that we ask FFmpeg to create in @@ -366,10 +365,28 @@ export interface Electron { */ ffmpegExec: ( command: FFmpegCommand, - dataOrPathOrZipItem: Uint8Array | string | ZipItem, + pathOrZipItem: string | ZipItem, outputFileExtension: string, ) => Promise; + /** + * Determine the duration (in seconds) of the video present at + * {@link pathOrZipItem} using ffmpeg. + * + * This is a bespoke variant of {@link ffmpegExec} for the sole purpose of + * retrieving the video duration. + * + * @param pathOrZipItem The input file whose duration we want to determine. + * For more details, see the documentation of the {@link ffmpegExec} + * parameter with the same name. + * + * @returns The duration (in seconds) of the video referred to by + * {@link pathOrZipItem}. + */ + ffmpegDetermineVideoDuration: ( + pathOrZipItem: string | ZipItem, + ) => Promise; + // - Utility process /** diff --git a/web/packages/base/utils/web.ts b/web/packages/base/utils/web.ts index f51583ae11..cb45280990 100644 --- a/web/packages/base/utils/web.ts +++ b/web/packages/base/utils/web.ts @@ -3,11 +3,12 @@ * folder by appending a temporary element to the DOM. * * @param url The URL that we want to download. See also - * {@link downloadAndRevokeObjectURL} and {@link downloadString}. + * {@link downloadAndRevokeObjectURL} and {@link downloadString}. The URL is + * revoked after initiating the download. * * @param fileName The name of downloaded file. */ -export const downloadURL = (url: string, fileName: string) => { +export const downloadAndRevokeObjectURL = (url: string, fileName: string) => { const a = document.createElement("a"); a.style.display = "none"; a.href = url; @@ -18,15 +19,6 @@ export const downloadURL = (url: string, fileName: string) => { a.remove(); }; -/** - * A variant of {@link downloadURL} that also revokes the provided - * {@link objectURL} after initiating the download. - */ -export const downloadAndRevokeObjectURL = (url: string, fileName: string) => { - downloadURL(url, fileName); - URL.revokeObjectURL(url); -}; - /** * Save the given string {@link s} as a file in the user's download folder. * diff --git a/web/packages/build-config/package.json b/web/packages/build-config/package.json index 4ab4368af4..9b66ba0489 100644 --- a/web/packages/build-config/package.json +++ b/web/packages/build-config/package.json @@ -4,15 +4,15 @@ "private": true, "type": "module", "devDependencies": { - "@eslint/js": "^9.25.1", - "eslint": "^9.25.1", + "@eslint/js": "^9.27.0", + "eslint": "^9.27.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "prettier": "^3.5.3", "prettier-plugin-organize-imports": "^4.1.0", - "prettier-plugin-packagejson": "^2.5.10", + "prettier-plugin-packagejson": "^2.5.14", "typescript": "^5.8.3", - "typescript-eslint": "^8.31.1" + "typescript-eslint": "^8.32.1" } } diff --git a/web/packages/gallery/components/FileInfo.tsx b/web/packages/gallery/components/FileInfo.tsx index fab7252efe..f4d4091189 100644 --- a/web/packages/gallery/components/FileInfo.tsx +++ b/web/packages/gallery/components/FileInfo.tsx @@ -37,9 +37,11 @@ import { import { LinkButtonUndecorated } from "ente-base/components/LinkButton"; import { type ButtonishProps } from "ente-base/components/mui"; import { ActivityIndicator } from "ente-base/components/mui/ActivityIndicator"; -import { SidebarDrawer } from "ente-base/components/mui/SidebarDrawer"; +import { + SidebarDrawer, + SidebarDrawerTitlebar, +} from "ente-base/components/mui/SidebarDrawer"; import { SingleInputForm } from "ente-base/components/SingleInputForm"; -import { Titlebar } from "ente-base/components/Titlebar"; import { EllipsizedTypography } from "ente-base/components/Typography"; import { useModalVisibility, @@ -256,9 +258,15 @@ export const FileInfo: React.FC = ({ onSelectPerson?.(personID); }; + const uploaderName = filePublicMagicMetadata(file)?.uploaderName; + return ( - + = ({ }} /> )} + {uploaderName && ( + + {t("added_by_name", { name: uploaderName })} + + )} = ({ const { values, errors, handleChange, handleSubmit, resetForm } = formik; if (!caption.length && !allowEdits) { - return <>; + // Visually take up some space, otherwise the info panel for the shared + // photos without a caption looks squished at the top. + return ; } return ( @@ -971,11 +989,11 @@ const RawExif: React.FC = ({ return ( - } diff --git a/web/packages/gallery/components/Upload.tsx b/web/packages/gallery/components/Upload.tsx new file mode 100644 index 0000000000..7f723875cf --- /dev/null +++ b/web/packages/gallery/components/Upload.tsx @@ -0,0 +1,7 @@ +/** + * The upload can be triggered by different buttons and flows in the UI, each of + * which is referred to as an "intent". + * + * The "intent" does not change the eventual upload outcome, only the UX flow. + */ +export type UploadTypeSelectorIntent = "upload" | "import" | "collect"; diff --git a/web/packages/gallery/components/viewer/data-source.ts b/web/packages/gallery/components/viewer/data-source.ts index 6c5d4a3727..d0f0d9a172 100644 --- a/web/packages/gallery/components/viewer/data-source.ts +++ b/web/packages/gallery/components/viewer/data-source.ts @@ -4,7 +4,7 @@ import { downloadManager } from "ente-gallery/services/download"; import { extractRawExif, parseExif } from "ente-gallery/services/exif"; import { hlsPlaylistDataForFile, - type HLSPlaylistData, + type HLSPlaylistDataForFile, } from "ente-gallery/services/video"; import type { EnteFile } from "ente-media/file"; import { fileCaption, filePublicMagicMetadata } from "ente-media/file-metadata"; @@ -452,10 +452,14 @@ const enqueueUpdates = async ( const updateVideo = ( videoURL: string | undefined, - hlsPlaylistData: HLSPlaylistData | undefined, + hlsPlaylistData: HLSPlaylistDataForFile, ) => { const videoURLD = videoURL ? { videoURL } : {}; - if (hlsPlaylistData) { + // See: [Note: Caching HLS playlist data] + // + // In brief, there are three cases: + if (typeof hlsPlaylistData == "object") { + // 1. If we have a playlist, we can cache it const { playlistURL: videoPlaylistURL, width, @@ -466,18 +470,9 @@ const enqueueUpdates = async ( createHLSPlaylistItemDataValidity(), ); } else { - // See: [Note: Caching HLS playlist data] - // - // TODO(HLS): As an optimization, we can handle the logged in vs - // public albums case separately once we have the status-diff state, - // we don't need to mark status-diff case as transient. - // - // Note that setting the transient flag is not too expensive, since - // the underlying videoURL is still cached by the download manager. - // So effectively, under normal circumstance, it just adds one API - // call (to recheck if an HLS playlist now exists for the given - // file). - update({ ...videoURLD, isTransient: true }); + // 2. if the file is not eligible ("skip"), we can cache it. + // 3. Otherwise it's transient and shouldn't be cached indefinitely. + update({ ...videoURLD, isTransient: hlsPlaylistData != "skip" }); } }; @@ -526,7 +521,7 @@ const enqueueUpdates = async ( } try { - let hlsPlaylistData: HLSPlaylistData | undefined; + let hlsPlaylistData: HLSPlaylistDataForFile; if (file.metadata.fileType == FileType.video) { hlsPlaylistData = await hlsPlaylistDataForFile( file, @@ -534,7 +529,10 @@ const enqueueUpdates = async ( ); // We have a HLS playlist, and the user didn't request the original. // Early return so that we don't initiate a fetch for the original. - if (hlsPlaylistData && opts?.videoQuality != "original") { + if ( + typeof hlsPlaylistData == "object" && + opts?.videoQuality != "original" + ) { updateVideo(undefined, hlsPlaylistData); return; } @@ -656,10 +654,10 @@ const thumbnailDimensions = ( return { width: thumbnailWidth, height: thumbnailHeight }; }; /** - * Return a new validity for a HLS playlist containing presigned URLs. + * Return a new validity for a HLS playlist containing pre-signed URLs. * * The content chunks in HLS playlist generated by - * {@link hlsPlaylistDataForFile} use presigned URLs generated by remote (see + * {@link hlsPlaylistDataForFile} use pre-signed URLs generated by remote (see * `PreSignedRequestValidityDuration` in the museum source). These have a * validity of 7 days. We keep a 2 day buffer, and consider any item data that * uses such playlist as stale after 5 days. diff --git a/web/packages/gallery/export-dirs.ts b/web/packages/gallery/export-dirs.ts new file mode 100644 index 0000000000..40229741ed --- /dev/null +++ b/web/packages/gallery/export-dirs.ts @@ -0,0 +1,11 @@ +/** + * Name of the directory in which we put our metadata when exporting to the file + * system. + */ +export const exportMetadataDirectoryName = "metadata"; + +/** + * Name of the directory in which we keep trash items when deleting files that + * have been exported to the local disk previously. + */ +export const exportTrashDirectoryName = "Trash"; diff --git a/web/packages/gallery/package.json b/web/packages/gallery/package.json index 8836f55c66..3bab643a54 100644 --- a/web/packages/gallery/package.json +++ b/web/packages/gallery/package.json @@ -7,20 +7,20 @@ "bs58": "^6.0.0", "ente-base": "*", "ente-utils": "*", - "exifreader": "^4.30.0", + "exifreader": "^4.30.1", "hls-video-element": "^1.5.1", "leaflet": "^1.9.4", "leaflet-defaulticon-compatibility": "^0.1.2", - "media-chrome": "^4.9.1", + "media-chrome": "^4.10.0", "photoswipe": "^5.4.4", "react": "^19.1.0", "react-dom": "^19.1.0", "react-dropzone": "14.2.10" }, "devDependencies": { - "@types/leaflet": "^1.9.17", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.3", + "@types/leaflet": "^1.9.18", + "@types/react": "^19.1.5", + "@types/react-dom": "^19.1.5", "ente-build-config": "*" } } diff --git a/web/packages/gallery/services/download.ts b/web/packages/gallery/services/download.ts index 52b6448c39..82f3dc1686 100644 --- a/web/packages/gallery/services/download.ts +++ b/web/packages/gallery/services/download.ts @@ -14,7 +14,7 @@ import { } from "ente-base/http"; import { ensureAuthToken } from "ente-base/local-user"; import log from "ente-base/log"; -import { customAPIOrigin } from "ente-base/origins"; +import { apiURL, customAPIOrigin } from "ente-base/origins"; import type { EnteFile } from "ente-media/file"; import { FileType } from "ente-media/file-type"; import { decodeLivePhoto } from "ente-media/live-photo"; @@ -343,8 +343,8 @@ class DownloadManager { * This is a convenience abstraction over {@link fileStream} that converts * it into a {@link Blob}. */ - async fileBlob(file: EnteFile) { - return this.fileStream(file).then((s) => new Response(s).blob()); + async fileBlob(file: EnteFile, opts?: FileDownloadOpts) { + return this.fileStream(file, opts).then((s) => new Response(s).blob()); } /** @@ -356,9 +356,12 @@ class DownloadManager { * cached for subsequent use. * * @param file The {@link EnteFile} whose data we want. + * + * @param opts Optional options to modify the download. */ async fileStream( file: EnteFile, + opts?: FileDownloadOpts, ): Promise | null> { const cachedURL = this.fileURLPromises.get(file.id); if (cachedURL) { @@ -372,7 +375,7 @@ class DownloadManager { } } - return this.downloadFile(file); + return this.downloadFile(file, opts); } /** @@ -397,10 +400,11 @@ class DownloadManager { private async downloadFile( file: EnteFile, + opts?: FileDownloadOpts, ): Promise | null> { log.info(`download attempted for file id ${file.id}`); - const res = await wrapErrors(() => this._downloadFile(file)); + const res = await wrapErrors(() => this._downloadFile(file, opts)); if ( file.metadata.fileType === FileType.image || @@ -423,8 +427,7 @@ class DownloadManager { const onDownloadProgress = this.trackDownloadProgress( file.id, - // TODO: Is info supposed to be optional though? - file.info?.fileSize ?? 0, + file.info?.fileSize, ); const contentLength = @@ -506,18 +509,26 @@ class DownloadManager { }); } - private async _downloadFile(file: EnteFile) { + /** + * Download the full contents of {@link file}, automatically choosing the + * credentials for the logged in user or the public albums depending on the + * current app context we are in. + */ + private async _downloadFile(file: EnteFile, opts?: FileDownloadOpts) { if (this.publicAlbumsCredentials) { return publicAlbums_downloadFile( file, this.publicAlbumsCredentials, ); } else { - return photos_downloadFile(file); + return photos_downloadFile(file, opts); } } - private trackDownloadProgress(fileID: number, fileSize: number) { + private trackDownloadProgress( + fileID: number, + fileSize: number | undefined, + ) { return (event: { loaded: number; total: number }) => { if (isNaN(event.total) || event.total === 0) { if (!fileSize) { @@ -676,7 +687,28 @@ const photos_downloadThumbnail = async (file: EnteFile) => { return new Uint8Array(await res.arrayBuffer()); }; -const photos_downloadFile = async (file: EnteFile): Promise => { +interface FileDownloadOpts { + /** + * `true` if the request is for a background task. These are considered less + * latency sensitive than user initiated interactive requests. + * + * See: [Note: User initiated vs background downloads of files]. + * + * This parameter is ignored for requests made when using public albums + * credentials to download files; those are always considered interactive. + */ + background?: boolean; +} + +/** + * Download the full contents of the given {@link EnteFile} + */ +const photos_downloadFile = async ( + file: EnteFile, + opts?: FileDownloadOpts, +): Promise => { + const { background } = opts ?? {}; + const customOrigin = await customAPIOrigin(); // [Note: Passing credentials for self-hosted file fetches] @@ -711,17 +743,24 @@ const photos_downloadFile = async (file: EnteFile): Promise => { // credentials in the "X-Auth-Token". // // 2. The proxy then does both the original steps: (a). Use the credentials - // to get the pre signed URL, and (b) fetch that pre signed URL and + // to get the pre-signed URL, and (b) fetch that pre-signed URL and // stream back the response. + // + // [Note: User initiated vs background downloads of files] + // + // The faster proxy approach is used for interactive requests to reduce the + // latency for the user (e.g. when the user is waiting to see a full + // resolution file). It can be faster than a direct connection as the proxy + // is network-nearer to the user (See: [Note: Faster uploads via workers]) + // + // For background processing (e.g., ML indexing, HLS generation), the direct + // S3 connection (as what'd happen when self hosting) gets used. const getFile = async () => { - if (customOrigin) { + if (customOrigin || background) { const token = await ensureAuthToken(); - const params = new URLSearchParams({ token }); - return fetch( - `${customOrigin}/files/download/${file.id}?${params.toString()}`, - { headers: publicRequestHeaders() }, - ); + const url = await apiURL(`/files/download/${file.id}`, { token }); + return fetch(url, { headers: publicRequestHeaders() }); } else { return fetch(`https://files.ente.io/?fileID=${file.id}`, { headers: await authenticatedRequestHeaders(), diff --git a/web/packages/gallery/services/ffmpeg/index.ts b/web/packages/gallery/services/ffmpeg/index.ts index a182ee1400..8c7817ee4e 100644 --- a/web/packages/gallery/services/ffmpeg/index.ts +++ b/web/packages/gallery/services/ffmpeg/index.ts @@ -2,7 +2,7 @@ import { ensureElectron } from "ente-base/electron"; import log from "ente-base/log"; import type { Electron } from "ente-base/types/ipc"; import { - toDataOrPathOrZipEntry, + toPathOrZipEntry, type FileSystemUploadItem, type UploadItem, } from "ente-gallery/services/upload"; @@ -20,7 +20,7 @@ import { inputPathPlaceholder, outputPathPlaceholder, } from "./constants"; -import { ffmpegExecWeb } from "./web"; +import { determineVideoDurationWeb, ffmpegExecWeb } from "./web"; /** * Generate a thumbnail for the given video using a Wasm FFmpeg running in a web @@ -74,7 +74,7 @@ export const generateVideoThumbnailNative = async ( _generateVideoThumbnail((seekTime: number) => electron.ffmpegExec( makeGenThumbnailCommand(seekTime), - toDataOrPathOrZipEntry(fsUploadItem), + toPathOrZipEntry(fsUploadItem), "jpeg", ), ); @@ -116,18 +116,17 @@ const _makeGenThumbnailCommand = (seekTime: number, forHDR: boolean) => [ ]; /** - * Extract metadata from the given video + * Extract metadata from the given video. * - * When we're running in the context of our desktop app _and_ we're passed a - * file path , this uses the native FFmpeg bundled with our desktop app. - * Otherwise it uses a Wasm build of FFmpeg running in a web worker. + * When we're running in the context of our desktop app _and_ we're passed an + * upload item that resolves to a path of the user's file system, this uses the + * native FFmpeg bundled with our desktop app. Otherwise it uses a Wasm build of + * FFmpeg running in a web worker. * - * This function is called during upload, when we need to extract the metadata - * of videos that the user is uploading. + * This function is called during upload, when we need to extract the + * "ffmetadata" of videos that the user is uploading. * - * @param uploadItem A {@link File}, or the absolute path to a file on the - * user's local file system. A path can only be provided when we're running in - * the context of our desktop app. + * @param uploadItem The video item being uploaded. */ export const extractVideoMetadata = async ( uploadItem: UploadItem, @@ -138,7 +137,7 @@ export const extractVideoMetadata = async ( ? await ffmpegExecWeb(command, uploadItem, "txt") : await ensureElectron().ffmpegExec( command, - toDataOrPathOrZipEntry(uploadItem), + toPathOrZipEntry(uploadItem), "txt", ), ); @@ -185,8 +184,13 @@ const parseFFmpegExtractedMetadata = (ffmpegOutput: Uint8Array) => { // with comments and newlines. // // https://ffmpeg.org/ffmpeg-formats.html#Metadata-2 + // + // On Windows, while I couldn't find it documented anywhere, the generated + // ffmetadata file uses Unix line separators ("\n"). But for the sake of + // extra (albeit possibly unnecessary) safety, handle both \r\n and \n + // separators in the split. See: [Note: ffmpeg newlines] - const lines = new TextDecoder().decode(ffmpegOutput).split("\n"); + const lines = new TextDecoder().decode(ffmpegOutput).split(/\r?\n/); const isPair = (xs: string[]): xs is [string, string] => xs.length == 2; const kvPairs = lines.map((property) => property.split("=")).filter(isPair); @@ -260,6 +264,26 @@ const parseFFMetadataDate = (s: string | undefined) => { return d; }; +/** + * Extract the duration (in seconds) from the given video + * + * This is a sibling of {@link extractVideoMetadata}, except it tries to + * determine the duration of the video. The duration is not part of the + * "ffmetadata", and is instead a property of the video itself. + * + * @param uploadItem The video item being uploaded. + * + * @return the duration of the video in seconds (a floating point number). + */ +export const determineVideoDuration = async ( + uploadItem: UploadItem, +): Promise => + uploadItem instanceof File + ? determineVideoDurationWeb(uploadItem) + : ensureElectron().ffmpegDetermineVideoDuration( + toPathOrZipEntry(uploadItem), + ); + /** * Convert a video from a format that is not supported in the browser to MP4. * diff --git a/web/packages/gallery/services/ffmpeg/web.ts b/web/packages/gallery/services/ffmpeg/web.ts index 09152e6e46..0074cc70ea 100644 --- a/web/packages/gallery/services/ffmpeg/web.ts +++ b/web/packages/gallery/services/ffmpeg/web.ts @@ -1,4 +1,5 @@ -import { FFmpeg } from "@ffmpeg/ffmpeg"; +import { FFFSType, FFmpeg } from "@ffmpeg/ffmpeg"; +import { joinPath } from "ente-base/file-name"; import { newID } from "ente-base/id"; import log from "ente-base/log"; import type { FFmpegCommand } from "ente-base/types/ipc"; @@ -14,7 +15,7 @@ import { let _ffmpeg: Promise | undefined; /** Queue of in-flight requests. */ -const _ffmpegTaskQueue = new PromiseQueue(); +const _ffmpegTaskQueue = new PromiseQueue(); /** * Return the shared {@link FFmpeg} instance, lazily creating and loading it if @@ -44,7 +45,7 @@ const createFFmpeg = async () => { * * @param command The FFmpeg command to execute. * - * @param blob The input data on which to run the command, provided as a blob. + * @param blob The input blob on which to run the command. * * @param outputFileExtension The extension of the (temporary) output file which * will be generated by the command. @@ -65,7 +66,26 @@ export const ffmpegExecWeb = async ( // So serialize them using a promise queue. return _ffmpegTaskQueue.add(() => ffmpegExec(ffmpeg, command, outputFileExtension, blob), - ); + ) as Promise; +}; + +/** + * Determine the duration of the given video blob. + * + * This is a specialized variant of {@link ffmpegExecWeb} that uses the same + * queue but internally uses ffprobe to try and determine the video's duration. + * + * @param blob The input blob on which to run the command, provided as a blob. + * + * @returns The duration of the {@link blob} (if it indeed is a video). + */ +export const determineVideoDurationWeb = async ( + blob: Blob, +): Promise => { + const ffmpeg = await ffmpegLazy(); + return _ffmpegTaskQueue.add(() => + ffprobeExecVideoDuration(ffmpeg, blob), + ) as Promise; }; const ffmpegExec = async ( @@ -74,63 +94,88 @@ const ffmpegExec = async ( outputFileExtension: string, blob: Blob, ) => { - const inputPath = newID("in_"); const outputSuffix = outputFileExtension ? "." + outputFileExtension : ""; const outputPath = newID("out_") + outputSuffix; - const inputData = new Uint8Array(await blob.arrayBuffer()); - // Exit status of the ffmpeg.exec invocation. // `0` if no error, `!= 0` if timeout (1) or error. let status: number | undefined; - try { - const startTime = Date.now(); + return withInputMount(ffmpeg, blob, async (inputPath) => { + try { + const startTime = Date.now(); - await ffmpeg.writeFile(inputPath, inputData); + let resolvedCommand: string[]; + if (Array.isArray(command)) { + resolvedCommand = command; + } else { + const isHDR = await isHDRVideo(ffmpeg, inputPath); + resolvedCommand = isHDR ? command.hdr : command.default; + } - let resolvedCommand: string[]; - if (Array.isArray(command)) { - resolvedCommand = command; - } else { - const isHDR = await isHDRVideo(ffmpeg, inputPath); - resolvedCommand = isHDR ? command.hdr : command.default; - } - - const cmd = substitutePlaceholders( - resolvedCommand, - inputPath, - outputPath, - ); - - status = await ffmpeg.exec(cmd); - if (status !== 0) { - log.info( - `[wasm] ffmpeg command failed with exit code ${status}: ${cmd.join(" ")}`, + const cmd = substitutePlaceholders( + resolvedCommand, + inputPath, + outputPath, ); - throw new Error(`ffmpeg command failed with exit code ${status}`); + + status = await ffmpeg.exec(cmd); + if (status !== 0) { + log.info( + `[wasm] ffmpeg command failed with exit code ${status}: ${cmd.join(" ")}`, + ); + throw new Error( + `ffmpeg command failed with exit code ${status}`, + ); + } + + const result = await ffmpeg.readFile(outputPath); + if (typeof result == "string") + throw new Error("Expected binary data"); + + const ms = Date.now() - startTime; + log.debug(() => `[wasm] ffmpeg ${cmd.join(" ")} (${ms} ms)`); + return result; + } finally { + try { + await ffmpeg.deleteFile(outputPath); + } catch (e) { + // Output file might not even exist if the command did not succeed, + // so only log on success. + if (status === 0) { + log.error(`Failed to remove output ${outputPath}`, e); + } + } } + }); +}; - const result = await ffmpeg.readFile(outputPath); - if (typeof result == "string") throw new Error("Expected binary data"); +const withInputMount = async ( + ffmpeg: FFmpeg, + blob: Blob, + f: (inputPath: string) => Promise, +): Promise => { + const mountDir = "/mount"; + const inputFileName = newID("in_"); + const inputPath = joinPath(mountDir, inputFileName); - const ms = Date.now() - startTime; - log.debug(() => `[wasm] ffmpeg ${cmd.join(" ")} (${ms} ms)`); - return result; + const inputFile = new File([blob], inputFileName); + + try { + await ffmpeg.createDir(mountDir); + await ffmpeg.mount(FFFSType.WORKERFS, { files: [inputFile] }, mountDir); + + return await f(inputPath); } finally { try { - await ffmpeg.deleteFile(inputPath); + await ffmpeg.unmount(mountDir); } catch (e) { - log.error(`Failed to remove input ${inputPath}`, e); + log.error(`Failed to remove mount ${mountDir}`, e); } try { - await ffmpeg.deleteFile(outputPath); + await ffmpeg.deleteDir(mountDir); } catch (e) { - // Output file might not even exist if the command did not succeed, - // so only log on success. - if (status === 0) { - log.error(`Failed to remove output ${outputPath}`, e); - } + log.error(`Failed to delete mount directory ${mountDir}`, e); } } }; @@ -154,7 +199,7 @@ const substitutePlaceholders = ( }) .filter((s) => s !== undefined); -const isHDRVideoFFProbeOutput = z.object({ +const FFProbeOutputIsHDR = z.object({ streams: z.array(z.object({ color_transfer: z.string().optional() })), }); @@ -171,8 +216,9 @@ const isHDRVideoFFProbeOutput = z.object({ * `false` to make this function safe to invoke without breaking the happy path. */ const isHDRVideo = async (ffmpeg: FFmpeg, inputFilePath: string) => { + let jsonString: string | undefined; try { - const jsonString = await ffprobeOutput( + jsonString = await ffprobeOutput( ffmpeg, [ ["-i", inputFilePath], @@ -182,7 +228,7 @@ const isHDRVideo = async (ffmpeg: FFmpeg, inputFilePath: string) => { // correct in a multi stream file because the ffmpeg automatic // mapping will use the highest resolution stream, but short of // reinventing ffmpeg's resolution mechanism, it is a reasonable - // assumption for our current, heuristic, check. + // assumption for our current heuristic check. ["-select_streams", "v:0"], // Output JSON ["-of", "json"], @@ -191,7 +237,7 @@ const isHDRVideo = async (ffmpeg: FFmpeg, inputFilePath: string) => { "output.json", ); - const output = isHDRVideoFFProbeOutput.parse(JSON.parse(jsonString)); + const output = FFProbeOutputIsHDR.parse(JSON.parse(jsonString)); switch (output.streams[0]?.color_transfer) { case "smpte2084": case "arib-std-b67": @@ -200,7 +246,8 @@ const isHDRVideo = async (ffmpeg: FFmpeg, inputFilePath: string) => { return false; } } catch (e) { - log.warn(`Could not detect HDR status of ${inputFilePath}`, e); + log.warn("Could not detect HDR status", e); + if (jsonString) log.debug(() => ["ffprobe-output", jsonString]); return false; } }; @@ -248,3 +295,51 @@ const ffprobeOutput = async ( } } }; + +const FFProbeOutputDuration = z.object({ + format: z.object({ duration: z.string() }), +}); + +const ffprobeExecVideoDuration = async (ffmpeg: FFmpeg, blob: Blob) => + withInputMount(ffmpeg, blob, async (inputPath) => { + // Determine the video duration from the container, bypassing the issues + // with stream selection. + // + // ffprobe -v error -show_entries format=duration -of + // default=noprint_wrappers=1:nokey=1 input.mp4 + // + // Source: + // https://trac.ffmpeg.org/wiki/FFprobeTips#Formatcontainerduration + // + // Reference: https://ffmpeg.org/ffprobe.html + // + // Since we cannot grab the stdout easily, the command has been modified + // to output to a file instead. However, in doing the command seems to + // have become flaky - for certain videos, it outputs extra lines and + // not just the duration. So we also switch to the JSON output for more + // robust behaviour, and parse the duration from it. + + const jsonString = await ffprobeOutput( + ffmpeg, + [ + ["-i", inputPath], + ["-v", "error"], + ["-show_entries", "format=duration"], + ["-of", "json"], + ["-o", "output.json"], + ].flat(), + "output.json", + ); + + const durationString = FFProbeOutputDuration.parse( + JSON.parse(jsonString), + ).format.duration; + + const duration = parseFloat(durationString); + if (isNaN(duration)) { + const msg = "Could not parse video duration"; + log.warn(msg, durationString); + throw new Error(msg); + } + return duration; + }); diff --git a/web/packages/gallery/services/file-data.ts b/web/packages/gallery/services/file-data.ts index 9972737d53..d2f0de2150 100644 --- a/web/packages/gallery/services/file-data.ts +++ b/web/packages/gallery/services/file-data.ts @@ -1,12 +1,15 @@ import { encryptBlobB64 } from "ente-base/crypto"; +import type { EncryptedBlobB64 } from "ente-base/crypto/types"; import { authenticatedPublicAlbumsRequestHeaders, authenticatedRequestHeaders, ensureOk, + retryEnsuringHTTPOk, type PublicAlbumsCredentials, } from "ente-base/http"; import { apiURL } from "ente-base/origins"; import type { EnteFile } from "ente-media/file"; +import { nullToUndefined } from "ente-utils/transform"; import { z } from "zod"; /** @@ -43,6 +46,29 @@ const RemoteFileData = z.object({ * crypto layer. */ decryptionHeader: z.string(), + /** + * The epoch microseconds when this file data entry was last upserted. + * + * [Note: PUT "mldata" version check] + * + * When PUT-ting mldata onto remote, the client is expected to pass the + * updated at of the existing {@link RemoteFileData} which it is updating + * (this field), or 0 if the client is creating a new entity. + * + * This allows remote to detect and reject cases where the client is trying + * to overwrite a version it hasn't yet pulled. + * + * About the optionality of this field: Newer museums are expected to always + * provide the {@link updatedAt} in the response, but for ease of self + * hosters we don't take a hard dependency on the latest museum and instead + * allow this field to be optional. When it is not present, effectively + * we'll pass 0 as {@link lastUpdatedAt} in the "mldata" PUT API call, but + * since it's an old museum it'll anyway ignore it. + * + * > This note was added May 2025, and the optionality can be removed in a + * > few months when museums should've updated (tag: Migration). + */ + updatedAt: z.number().nullish().transform(nullToUndefined), }); type RemoteFileData = z.infer; @@ -100,7 +126,13 @@ export const fetchFileData = async ( fileID: number, publicAlbumsCredentials?: PublicAlbumsCredentials, ): Promise => { - const params = new URLSearchParams({ type, fileID: fileID.toString() }); + const params = new URLSearchParams({ + type, + fileID: fileID.toString(), + // Ask museum to respond with 204 instead of 404 if no playlist exists + // for the given file. + preferNoContent: "true", + }); let res: Response; if (publicAlbumsCredentials) { @@ -116,6 +148,11 @@ export const fetchFileData = async ( }); } + if (res.status == 204) return undefined; + // We're passing `preferNoContent` so the expected response is 204, but this + // might be a self hoster running an older museum that does not recognize + // that flag, so retain the old behavior. This fallback can be removed in a + // few months (tag: Migration, note added May 2025). if (res.status == 404) return undefined; ensureOk(res); return z.object({ data: RemoteFileData }).parse(await res.json()).data; @@ -126,9 +163,21 @@ export const fetchFileData = async ( * structure has more fields, there are just the fields we are interested in. */ const RemoteFDStatus = z.object({ + /** + * The ID of the file whose file data we're querying. + */ fileID: z.number(), - /** Expected to be one of {@link FileDataType} */ + /** + * Expected to be one of {@link FileDataType} + */ type: z.string(), + /** + * `true` if the file data has been deleted. + * + * This can be true in the in-progress partial deletion case, which the file + * data deletion has been processed but the file deletion has not yet been + * processed. + */ isDeleted: z.boolean(), /** * The epoch microseconds when this file data entry was added or updated. @@ -147,7 +196,8 @@ export interface UpdatedFileDataFileIDsPage { fileIDs: Set; /** * The latest updatedAt (epoch microseconds) time obtained from remote in - * this batch this sync from amongst all of these files. + * this batch of sync (from amongst all of the files in the batch, not just + * those that were filtered to be part of {@link fileIDs}). */ lastUpdatedAt: number; } @@ -167,18 +217,25 @@ export interface UpdatedFileDataFileIDsPage { * Set this to zero to start from the beginning. * * @param onPage A callback invoked for each page of results received from - * remote. It is passed both the fileIDs received in the batch under - * consideration, and the largest of the updated time for all entries - * (irrespective of {@link type}) in that batch. + * remote. It is passed the fileIDs received in the batch under consideration, + * and the largest of the updated time for all entries (irrespective of + * {@link type}) in that batch. * * ---- * - * Implementation notes: + * [Note: Pruning stale status-diff entries] * * Unlike other "diff" APIs, the diff API used here won't return tombstone * entries for deleted files. This is not a problem because there are no current * cases where existing playlists or ML indexes get deleted (unless the * underlying file is deleted). See: [Note: Caching HLS playlist data]. + * + * Note that the "/files/data/status-diff" includes entries for files that are + * in trash. This means that, while not a practical problem (because it's just + * numeric ids), the number of fileIDs we store locally can grow unbounded as + * files move to trash and then get deleted. So to prune them, we also add a + * hook to the /trash/v2/diff processing, and prune any locally saved file IDs + * which have been deleted from trash. */ export const syncUpdatedFileDataFileIDs = async ( type: FileDataType, @@ -199,6 +256,9 @@ export const syncUpdatedFileDataFileIDs = async ( const fileIDs = new Set(); for (const fd of diff) { lastUpdatedAt = Math.max(lastUpdatedAt, fd.updatedAt); + // While we could prune isDeleted entries here, we can also rely + // on the pruning that happens when the trash gets synced. See: + // [Note: Pruning stale status-diff entries] if (fd.type == type && !fd.isDeleted) { fileIDs.add(fd.fileID); } @@ -223,11 +283,15 @@ export const syncUpdatedFileDataFileIDs = async ( * * @param data The binary data to upload. The exact contents of the data are * {@link type} specific. + * + * @param lastUpdatedAt The {@link updatedAt} of the {@link RemoteFileData} + * which we are updating, or 0 to indicate a new entity. */ export const putFileData = async ( file: EnteFile, type: FileDataType, data: Uint8Array, + lastUpdatedAt: number, ) => { const { encryptedData, decryptionHeader } = await encryptBlobB64( data, @@ -242,6 +306,7 @@ export const putFileData = async ( type, encryptedData, decryptionHeader, + lastUpdatedAt, }), }); ensureOk(res); @@ -258,7 +323,7 @@ export const putFileData = async ( * context of the public albums app. If these are not specified, then the * credentials of the logged in user are used. * - * @returns the (presigned) URL to the preview data, or undefined if there is + * @returns the (pre-signed) URL to the preview data, or undefined if there is * not preview data of the given type for the given file yet. * * [Note: File data vs file preview data] @@ -314,36 +379,6 @@ export const fetchFilePreviewData = async ( return z.object({ url: z.string() }).parse(await res.json()).url; }; -const FilePreviewDataUploadURLResponse = z.object({ - /** - * The objectID with which this uploaded data can be referred to post upload - * (e.g. when invoking {@link putVideoData}). - */ - objectID: z.string(), - /** - * A presigned URL that can be used to upload the file. - */ - url: z.string(), -}); - -/** - * Obtain a presigned URL that can be used to upload the "file preview data" of - * type "vid_preview" (the file containing the encrypted video segments which - * the "vid_preview" HLS playlist for the file would refer to). - */ -export const getFilePreviewDataUploadURL = async (file: EnteFile) => { - const params = new URLSearchParams({ - fileID: `${file.id}`, - type: "vid_preview", - }); - const url = await apiURL("/files/data/preview-upload-url"); - const res = await fetch(`${url}?${params.toString()}`, { - headers: await authenticatedRequestHeaders(), - }); - ensureOk(res); - return FilePreviewDataUploadURLResponse.parse(await res.json()); -}; - /** * Update the video data associated with the given file to remote. * @@ -361,8 +396,8 @@ export const getFilePreviewDataUploadURL = async (file: EnteFile) => { * * @param file {@link EnteFile} which this data is associated with. * - * @param playlistData The playlist data, suitably encoded in a form ready for - * encryption. + * @param encryptedPlaylist The encrypted playlist data (along with the nonce + * used during encryption). * * @param objectID Object ID of an already uploaded "file preview data" (see * {@link getFilePreviewDataUploadURL}). @@ -372,25 +407,22 @@ export const getFilePreviewDataUploadURL = async (file: EnteFile) => { */ export const putVideoData = async ( file: EnteFile, - playlistData: Uint8Array, + encryptedPlaylist: EncryptedBlobB64, objectID: string, objectSize: number, -) => { - const { encryptedData, decryptionHeader } = await encryptBlobB64( - playlistData, - file.key, +) => + retryEnsuringHTTPOk( + async () => + fetch(await apiURL("/files/video-data"), { + method: "PUT", + headers: await authenticatedRequestHeaders(), + body: JSON.stringify({ + fileID: file.id, + objectID, + objectSize, + playlist: encryptedPlaylist.encryptedData, + playlistHeader: encryptedPlaylist.decryptionHeader, + }), + }), + { retryProfile: "background" }, ); - - const res = await fetch(await apiURL("/files/video-data"), { - method: "PUT", - headers: await authenticatedRequestHeaders(), - body: JSON.stringify({ - fileID: file.id, - objectID, - objectSize, - playlist: encryptedData, - playlistHeader: decryptionHeader, - }), - }); - ensureOk(res); -}; diff --git a/web/packages/gallery/services/upload/index.ts b/web/packages/gallery/services/upload/index.ts index 57c50e5f8c..df1e6c6fe7 100644 --- a/web/packages/gallery/services/upload/index.ts +++ b/web/packages/gallery/services/upload/index.ts @@ -1,6 +1,8 @@ +import { basename, dirname } from "ente-base/file-name"; import log from "ente-base/log"; import { customAPIOrigin } from "ente-base/origins"; import type { ZipItem } from "ente-base/types/ipc"; +import { exportMetadataDirectoryName } from "ente-gallery/export-dirs"; import type { Collection } from "ente-media/collection"; import { nullToUndefined } from "ente-utils/transform"; import { z } from "zod"; @@ -111,6 +113,139 @@ export interface TimestampedFileSystemUploadItem { lastModifiedMs: number; } +/** + * An "path prefix"-like opaque string which can be used to disambiguate + * distinct source {@link UploadItem}s with the same name that are meant to be + * uploaded to the same destination Ente album. + * + * Th documentation of {@link UploadItem} describes the four cases that an + * {@link UploadItem} can be. For each of these, we augment an + * {@link UploadItem} with a prefix ("dirname") derived from its best "path": + * + * - Relative path or name in the case of web {@link File}s. + * + * - Absolute path in the case of desktop {@link File} or path or + * {@link FileAndPath}. + * + * - Path within the zip file for desktop {@link ZipItem}s. + * + * Thus, this path should not be treated as an address that can be used to + * retrieve the upload item, but rather as extra context that can help us + * distinguish between items by their relative or path prefix when their file + * names are the same and they're being uploaded to the same album. + * + * Consider the following hierarchy: + * + * Foo/2017/Album1/1.png + * Foo/2017/Album1/1.png.json + * + * Foo/2020/Album1/1.png + * Foo/2020/Album1/1.png.json + * + * If user uploads `Foo`, irrespective of if they select the "root" or "parent" + * option {@link CollectionMapping} option, when matching the takeout, only the + * Ente album is considered. So it will be undefined which JSON will get used, + * and both PNG files will get the same JSON, not their file system siblings. + * + * In such cases, the path prefix of the item being uploaded can act as extra + * context that can help us disambiguate and pick the sibling. Note how we don't + * need the path prefix to be absolute or relative or even addressable, we just + * need it as extra context that can help us disambiguate two items with + * otherwise the same name and that are destined for the same Ente album. + * + * So for our example, the path prefixes will be + * + * Foo/2017/Album1/1.png "Foo/2017/Album1" + * Foo/2017/Album1/1.png.json "Foo/2017/Album1" + * + * Foo/2020/Album1/1.png "Foo/2020/Album1" + * Foo/2020/Album1/1.png.json "Foo/2020/Album1" + * + * And can thus be used to associate the correct metadata JSON with the + * corresponding {@link UploadItem}. + * + * As a special case, "/metadata" at the end of the path prefix is discarded. + * This allows the metadata JSON written by export to be read back in during + * uploads. See: [Note: Fold "metadata" directory into parent folder]. + */ +export type UploadPathPrefix = string; + +/** + * Return the {@link UploadPathPrefix} for the given {@link pathOrName} of an + * item being uploaded. + */ +export const uploadPathPrefix = (pathOrName: string) => { + const folderPath = dirname(pathOrName); + if (basename(folderPath) == exportMetadataDirectoryName) { + return dirname(folderPath); + } + return folderPath; +}; + +export type UploadItemAndPath = [UploadItem, string]; + +/** + * Group files that are that have the same parent folder into collections. + * + * This is used to segregate the list of {@link UploadItemAndPath}s that we + * obtain when an upload is initiated into per-collection groups when the user + * chooses the "parent" {@link CollectionMapping} option. + * + * For example, if the user selects files have a directory structure like: + * + * a + * / | \ + * b j c + * /|\ / \ + * e f g h i + * + * The files will grouped into 3 collections: + * + * [ + * a => [j], + * b => [e, f, g], + * c => [h, i] + * ] + * + * @param defaultFolderName Optional collection name to use for any rooted files + * that do not have a parent folder. The function will throw if a default is not + * provided and we encounter any such files without a parent. + */ +export const groupItemsBasedOnParentFolder = ( + uploadItemAndPaths: UploadItemAndPath[], + defaultFolderName: string | undefined, +) => { + const result = new Map(); + for (const [uploadItem, pathOrName] of uploadItemAndPaths) { + const folderPath = dirname(pathOrName); + let folderName = basename(folderPath); + // [Note: Fold "metadata" directory into parent folder] + // + // If the parent folder of a file is "metadata" (the directory in which + // the exported JSON files are written), then we consider it to be part + // of the parent folder. + // + // e.g. for the file list + // + // [a/x.png, a/metadata/x.png.json] + // + // we want both to be grouped into the collection "a". This is so that + // we can cluster the metadata JSON files in the same collection as the + // file it is for. + if (folderName == exportMetadataDirectoryName) { + folderName = basename(dirname(folderPath)); + } + if (!folderName) { + if (!defaultFolderName) + throw Error(`Leaf file (without default): ${pathOrName}`); + folderName = defaultFolderName; + } + if (!result.has(folderName)) result.set(folderName, []); + result.get(folderName)!.push([uploadItem, pathOrName]); + } + return result; +}; + export interface LivePhotoAssets { image: UploadItem; video: UploadItem; @@ -127,6 +262,7 @@ export interface ClusteredUploadItem { fileName: string; isLivePhoto: boolean; uploadItem?: UploadItem; + pathPrefix: UploadPathPrefix | undefined; // TODO: Tie this to the isLivePhoto flag using a discriminated union. livePhotoAssets?: LivePhotoAssets; } @@ -283,13 +419,11 @@ export const fileSystemUploadItemIfUnchanged = async ( * context of our desktop app, return a value that can be passed to * {@link Electron} functions over IPC. */ -export const toDataOrPathOrZipEntry = (fsUploadItem: FileSystemUploadItem) => +export const toPathOrZipEntry = (fsUploadItem: FileSystemUploadItem) => typeof fsUploadItem == "string" || Array.isArray(fsUploadItem) ? fsUploadItem : fsUploadItem.path; -export const RANDOM_PERCENTAGE_PROGRESS_FOR_PUT = () => 90 + 10 * Math.random(); - export type UploadPhase = | "preparing" | "readingMetadata" @@ -311,6 +445,8 @@ export type UploadResult = /** * Return true to disable the upload of files via Cloudflare Workers. * + * [Note: Faster uploads via workers] + * * These workers were introduced as a way of make file uploads faster: * https://ente.io/blog/tech/making-uploads-faster/ * diff --git a/web/packages/gallery/services/upload/takeout.ts b/web/packages/gallery/services/upload/metadata-json.ts similarity index 81% rename from web/packages/gallery/services/upload/takeout.ts rename to web/packages/gallery/services/upload/metadata-json.ts index 8f85f7f465..277dff289a 100644 --- a/web/packages/gallery/services/upload/takeout.ts +++ b/web/packages/gallery/services/upload/metadata-json.ts @@ -4,15 +4,23 @@ import { ensureElectron } from "ente-base/electron"; import { nameAndExtension } from "ente-base/file-name"; import log from "ente-base/log"; import { type Location } from "ente-base/types"; -import type { UploadItem } from "ente-gallery/services/upload"; +import type { + UploadItem, + UploadPathPrefix, +} from "ente-gallery/services/upload"; import { readStream } from "ente-gallery/utils/native-stream"; /** * The data we read from the JSON metadata sidecar files. * - * Originally these were used to read the JSON metadata sidecar files present in - * a Google Takeout. However, during our own export, we also write out files - * with a similar structure. + * The metadata JSON file format is similar to the JSON metadata sidecar format + * produced by Google Takeout. This is so that the user can import their Google + * Takeouts while preserving the metadata. + * + * During export the Ente app also writes out its own metadata in a similar and + * compatible format so that the user can easily round trip (i.e. export their + * data out of Ente, and import it back again). This also allows users to reuse + * existing third party tools for working with the takeout format. */ export interface ParsedMetadataJSON { creationTime?: number; @@ -25,29 +33,54 @@ export interface ParsedMetadataJSON { * Derive a key for the given {@link jsonFileName} that should be used to index * into the {@link ParsedMetadataJSON} JSON map. * + * @param pathPrefix The {@link UploadPathPrefix}, if available. + * * @param collectionID The collection to which we're uploading. * * @param jsonFileName The file name for the JSON file. * * @returns A key suitable for indexing into the metadata JSON map. + * + * @see also {@link matchJSONMetadata}. */ export const metadataJSONMapKeyForJSON = ( + pathPrefix: UploadPathPrefix | undefined, collectionID: number, jsonFileName: string, -) => `${collectionID}-${jsonFileName.slice(0, -1 * ".json".length)}`; +) => + makeKey3( + pathPrefix, + collectionID, + jsonFileName.slice(0, -1 * ".json".length), + ); + +const makeKey3 = ( + pathPrefix: UploadPathPrefix | undefined, + collectionID: number, + fileName: string, +) => `${pathPrefix ?? ""}-${collectionID}-${fileName}`; /** * Return the matching entry, if any, from {@link parsedMetadataJSONMap} for the - * {@link fileName} and {@link collectionID} combination. + * {@link pathPrefix}, {@link collectionID} and {@link fileName} combination. * * This is the sibling of {@link metadataJSONMapKeyForJSON}, except for deriving * the filename key we might have to try a bunch of different variations, so * this does not return a single key but instead tries the combinations until it * finds an entry in the map, and returns the found entry instead of the key. + * + * In brief, + * + * - When inserting the metadata entry into {@link parsedMetadataJSONMap}, we + * use {@link metadataJSONMapKeyForJSON}. + * + * - When reading back the metadata entry from {@link parsedMetadataJSONMap}, we + * use {@link matchJSONMetadata} */ -export const matchTakeoutMetadata = ( - fileName: string, +export const matchJSONMetadata = ( + pathPrefix: UploadPathPrefix | undefined, collectionID: number, + fileName: string, parsedMetadataJSONMap: Map, ) => { // Break the fileName down into its components. @@ -85,9 +118,13 @@ export const matchTakeoutMetadata = ( name = name.slice(0, -1 * editedFileSuffix.length); } + // Convenience function to vary only the file name component when + // constructing a key. Delegates to `makeKey3`. + const makeKey = (fn: string) => makeKey3(pathPrefix, collectionID, fn); + // Derive a key from the collection name, file name and the suffix if any. let baseFileName = `${name}${extension}`; - let key = `${collectionID}-${baseFileName}${numberedSuffix}`; + let key = makeKey(`${baseFileName}${numberedSuffix}`); let takeoutMetadata = parsedMetadataJSONMap.get(key); if (takeoutMetadata) return takeoutMetadata; @@ -97,7 +134,9 @@ export const matchTakeoutMetadata = ( // the clipped file name to get the key. const maxGoogleFileNameLength = 46; - key = `${collectionID}-${baseFileName.slice(0, maxGoogleFileNameLength)}${numberedSuffix}`; + key = makeKey( + `${baseFileName.slice(0, maxGoogleFileNameLength)}${numberedSuffix}`, + ); takeoutMetadata = parsedMetadataJSONMap.get(key); if (takeoutMetadata) return takeoutMetadata; @@ -118,7 +157,9 @@ export const matchTakeoutMetadata = ( const supplSuffix = ".supplemental-metadata"; baseFileName = `${name}${extension}${supplSuffix}`; - key = `${collectionID}-${baseFileName.slice(0, maxGoogleFileNameLength)}${numberedSuffix}`; + key = makeKey( + `${baseFileName.slice(0, maxGoogleFileNameLength)}${numberedSuffix}`, + ); takeoutMetadata = parsedMetadataJSONMap.get(key); return takeoutMetadata; diff --git a/web/packages/gallery/services/upload/remote.ts b/web/packages/gallery/services/upload/remote.ts index 976a7ca3b8..abe934ad2d 100644 --- a/web/packages/gallery/services/upload/remote.ts +++ b/web/packages/gallery/services/upload/remote.ts @@ -1,29 +1,38 @@ // TODO: Audit this file /* eslint-disable @typescript-eslint/ban-ts-comment */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-return */ import { authenticatedPublicAlbumsRequestHeaders, authenticatedRequestHeaders, ensureOk, + publicRequestHeaders, retryAsyncOperation, + type HTTPRequestRetrier, type PublicAlbumsCredentials, } from "ente-base/http"; import log from "ente-base/log"; import { apiURL, uploaderOrigin } from "ente-base/origins"; import { type EnteFile } from "ente-media/file"; -import { CustomError, handleUploadError } from "ente-shared/error"; +import { handleUploadError } from "ente-shared/error"; import HTTPService from "ente-shared/network/HTTPService"; +import { nullToUndefined } from "ente-utils/transform"; import { z } from "zod"; -import type { MultipartUploadURLs, UploadFile } from "./upload-service"; +import type { UploadFile } from "./upload-service"; /** - * A pre-signed URL alongwith the associated object key. + * A pre-signed URL alongwith the associated object key that is later used to + * refer to file contents (the "object") that were uploaded to this URL. */ const ObjectUploadURL = z.object({ - /** A pre-signed URL that can be used to upload data to S3. */ + /** + * The objectKey with which remote (both museum and the S3 bucket) will + * refer to this object once it has been uploaded. + */ objectKey: z.string(), - /** The objectKey with which remote will refer to this object. */ + /** + * A pre-signed URL that can be used to upload data to an S3-compatible + * remote. + */ url: z.string(), }); @@ -31,6 +40,377 @@ export type ObjectUploadURL = z.infer; const ObjectUploadURLResponse = z.object({ urls: ObjectUploadURL.array() }); +/** + * Fetch a fresh list of URLs from remote that can be used to upload objects. + * + * @param countHint An approximate number of objects that we're expecting to + * upload. + * + * @returns A list of pre-signed object URLs that can be used to upload data to + * the S3 bucket. Each URL also has an associated "object key" with which remote + * will refer to the uploaded object after it has been uploaded. + */ +export const fetchUploadURLs = async (countHint: number) => { + const count = Math.min(50, countHint * 2).toString(); + const params = new URLSearchParams({ count }); + const url = await apiURL("/files/upload-urls"); + const res = await fetch(`${url}?${params.toString()}`, { + headers: await authenticatedRequestHeaders(), + }); + ensureOk(res); + return ObjectUploadURLResponse.parse(await res.json()).urls; +}; + +/** + * Sibling of {@link fetchUploadURLs} for public albums. + */ +export const fetchPublicAlbumsUploadURLs = async ( + countHint: number, + credentials: PublicAlbumsCredentials, +) => { + const count = Math.min(50, countHint * 2).toString(); + const params = new URLSearchParams({ count }); + const url = await apiURL("/public-collection/upload-urls"); + const res = await fetch(`${url}?${params.toString()}`, { + headers: authenticatedPublicAlbumsRequestHeaders(credentials), + }); + ensureOk(res); + return ObjectUploadURLResponse.parse(await res.json()).urls; +}; + +/** + * A list of URLs to use for multipart uploads. + * + * This is a list of pre-signed URLs (one for each part), a URL to indicate + * completion, and an associated object key that is later used to refer to the + * combined object from the parts there were uploaded to the part URLs. + */ +const MultipartUploadURLs = z.object({ + /** + * The objectKey with which remote (museum and the S3 bucket) will refer to + * this object once it has been uploaded. + */ + objectKey: z.string(), + /** + * A list of pre-signed URLs that can be used to upload the parts of the + * entire file's data to an S3-compatible remote. + */ + partURLs: z.string().array(), + /** + * A pre-signed URL that can be used to finalize the multipart upload into a + * single object on remote by providing the list of parts that were uploaded + * (and their sequence) to the S3-compatible remote. + */ + completeURL: z.string(), +}); + +export type MultipartUploadURLs = z.infer; + +const MultipartUploadURLsResponse = z.object({ urls: MultipartUploadURLs }); + +/** + * Fetch a {@link MultipartUploadURLs} structure from remote that can be used to + * upload a large object by splitting it into {@link uploadPartCount} parts. + * + * See: [Note: Multipart uploads]. + * + * @param uploadPartCount The number of parts in which we want to upload the + * object. + * + * @returns A structure ({@link MultipartUploadURLs}) containing pre-signed URLs + * for uploading each part, a completion URL, and the final object key. + */ +export const fetchMultipartUploadURLs = async (uploadPartCount: number) => { + const params = new URLSearchParams({ count: uploadPartCount.toString() }); + const url = await apiURL("/files/multipart-upload-urls"); + const res = await fetch(`${url}?${params.toString()}`, { + headers: await authenticatedRequestHeaders(), + }); + ensureOk(res); + return MultipartUploadURLsResponse.parse(await res.json()).urls; +}; + +/** + * Sibling of {@link fetchMultipartUploadURLs} for public albums. + */ +export const fetchPublicAlbumsMultipartUploadURLs = async ( + uploadPartCount: number, + credentials: PublicAlbumsCredentials, +) => { + const params = new URLSearchParams({ count: uploadPartCount.toString() }); + const url = await apiURL("/public-collection/multipart-upload-urls"); + const res = await fetch(`${url}?${params.toString()}`, { + headers: authenticatedPublicAlbumsRequestHeaders(credentials), + }); + ensureOk(res); + return MultipartUploadURLsResponse.parse(await res.json()).urls; +}; + +/** + * Upload a file using a pre-signed URL. + * + * @param fileUploadURL A pre-signed URL that can be used to upload data to the + * remote S3-compatible storage. + * + * @param fileData The data to upload. + * + * @param retrier A function to wrap the request in retries if needed. + */ +export const putFile = async ( + fileUploadURL: string, + fileData: Uint8Array, + retrier: HTTPRequestRetrier, +) => + retrier(() => + fetch(fileUploadURL, { + method: "PUT", + headers: publicRequestHeaders(), + body: fileData, + }), + ); + +/** + * Variant of {@link putFile} that uses a CF worker. + */ +export const putFileViaWorker = async ( + fileUploadURL: string, + fileData: Uint8Array, + retrier: HTTPRequestRetrier, +) => + retrier(async () => + fetch(`${await uploaderOrigin()}/file-upload`, { + method: "PUT", + headers: { ...publicRequestHeaders(), "UPLOAD-URL": fileUploadURL }, + body: fileData, + }), + ); + +/** + * Upload a part of a multipart upload using a pre-signed URL. + * + * See: [Note: Multipart uploads]. + * + * @param partUploadURL A pre-signed URL that can be used to upload data to the + * remote S3-compatible storage. + * + * @param partData The part bytes to upload. + * + * @param retrier A function to wrap the request in retries if needed. + * + * @returns the value of the "ETag" header in the remote response, or + * `undefined` if the ETag was not present in the response (this is not expected + * from remote in case of a successful response, but it can happen in case the + * user has some misconfigured browser extension which is blocking the ETag + * header from being parsed). + */ +export const putFilePart = async ( + partUploadURL: string, + partData: Uint8Array, + retrier: HTTPRequestRetrier, +) => { + const res = await retrier(() => + fetch(partUploadURL, { + method: "PUT", + headers: publicRequestHeaders(), + body: partData, + }), + ); + return nullToUndefined(res.headers.get("etag")); +}; + +/** + * Variant of {@link putFilePart} that uses a CF worker. + */ +export const putFilePartViaWorker = async ( + partUploadURL: string, + partData: Uint8Array, + retrier: HTTPRequestRetrier, +) => { + const origin = await uploaderOrigin(); + const res = await retrier(() => + fetch(`${origin}/multipart-upload`, { + method: "PUT", + headers: { ...publicRequestHeaders(), "UPLOAD-URL": partUploadURL }, + body: partData, + }), + ); + return z.object({ etag: z.string() }).parse(await res.json()).etag; +}; + +/** + * Information about an individual part of a multipart upload that has been + * uploaded to the remote (S3 or proxy). + * + * See: [Note: Multipart uploads]. + */ +export interface MultipartCompletedPart { + /** + * The part number (1-indexed). + * + * The part number indicates the sequential ordering where this part belongs + * in the overall file's data. + */ + partNumber: number; + /** + * The part "ETag". + * + * This is the Entity tag (retrieved as the "ETag" response header) returned + * by remote when the part was uploaded. + */ + eTag: string; +} + +/** + * Construct an XML string of the format expected as the request body for + * {@link _completeMultipartUpload} or + * {@link _completeMultipartUploadViaWorker}. + * + * @param parts Information about the parts that were uploaded. + */ +const createMultipartUploadRequestBody = ( + parts: MultipartCompletedPart[], +): string => { + // To avoid introducing a dependency on a XML library, we construct the + // requisite XML by hand. + // + // Example: + // + // + // + // 1 + // "1b3e6cdb1270c0b664076f109a7137c1" + // + // + // 2 + // "6049d6384a9e65694c833a3aca6584fd" + // + // + // 3 + // "331747eae8068f03b844e6f28cc0ed23" + // + // + // + // + // Spec: + // https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompleteMultipartUpload.html + // + // + // + // integer + // string + // + // ... + // + // + // Note that in the example given on the spec page, the etag strings are quoted: + // + // + // + // 1 + // "a54357aff0632cce46d942af68356b38" + // + // ... + // + // + // No extra quotes need to be added, the etag values we get from remote + // already quoted, we just need to pass them verbatim. + + const resultParts = parts.map( + (part) => + `${part.partNumber}${part.eTag}`, + ); + return `\n${resultParts.join("\n")}\n`; +}; + +/** + * Complete a multipart upload by reporting information about all the uploaded + * parts to the provided {@link completionURL}. + * + * @param completionURL A pre-signed URL to which the final status of the + * uploaded parts should be reported to. + * + * @param completedParts Information about all the parts of the file that have + * been uploaded. The part numbers must start at 1 and must be consecutive. + * + * @param retrier A function to wrap the request in retries if needed. + * + * [Note: Multipart uploads] + * + * Multipart uploads are a mechanism to upload large files onto an remote + * storage bucket by breaking it into smaller chunks / "parts", uploading each + * part separately, and then reporting the consolidated information of all the + * uploaded parts to a URL that marks the upload as complete on remote. + * + * This allows greater resilience since uploads of individual parts can be + * retried independently without failing the entire upload on transient network + * issues. This also helps self hosters, since often cloud providers have limits + * to the size of single requests that they'll allow through (e.g. the + * Cloudflare free plan currently has a 100 MB request size limit). + * + * The flow is implemented in two ways: + * + * a. The normal way, where each requests is made to a remote S3 bucket directly + * using the pre-signed URL. + * + * b. Using workers, where the requests are proxied via a worker near to the + * user's network to speed the requests up. + * + * See [Note: Faster uploads via workers] for more details on the worker flow. + * + * In both cases, the overall flow is roughly like the following: + * + * 1. Obtain multiple pre-signed URLs from remote (museum). The specific API + * call will be different (because of the different authentication + * mechanisms) when we're running in the context of the photos app + * ({@link fetchMultipartUploadURLs}) and when we're running in the context + * of the public albums app ({@link fetchPublicAlbumsMultipartUploadURLs}). + * + * 2. Break the file to be uploaded into parts, and upload each part using a PUT + * request to one of the pre-signed URLs we got in step 1. There are two + * variants of this - one where we directly upload to the remote (S3) + * ({@link putFilePart}), and one where we go via a worker + * ({@link putFilePartViaWorker}). + * + * 3. Once all the parts have been uploaded, send a consolidated report of all + * the uploaded parts (the step 2's) to remote via another pre-signed + * "completion URL" that we also got in step 1. Like step 2, there are 2 + * variants of this - one where we directly tell the remote (S3) + * ({@link completeMultipartUpload}), and one where we report via a worker + * ({@link completeMultipartUploadViaWorker}). + */ +export const completeMultipartUpload = ( + completionURL: string, + completedParts: MultipartCompletedPart[], + retrier: HTTPRequestRetrier, +) => + retrier(() => + fetch(completionURL, { + method: "POST", + headers: { ...publicRequestHeaders(), "Content-Type": "text/xml" }, + body: createMultipartUploadRequestBody(completedParts), + }), + ); + +/** + * Variant of {@link completeMultipartUpload} that uses a CF worker. + */ +export const completeMultipartUploadViaWorker = async ( + completionURL: string, + completedParts: MultipartCompletedPart[], + retrier: HTTPRequestRetrier, +) => + retrier(async () => + fetch(`${await uploaderOrigin()}/multipart-complete`, { + method: "POST", + headers: { + ...publicRequestHeaders(), + "Content-Type": "text/xml", + "UPLOAD-URL": completionURL, + }, + body: createMultipartUploadRequestBody(completedParts), + }), + ); + /** * Lowest layer for file upload related HTTP operations when we're running in * the context of the photos app. @@ -49,7 +429,7 @@ export class PhotosUploadHTTPClient { null, headers, ), - handleUploadError, + { abortIfNeeded: handleUploadError }, ); return response.data; } catch (e) { @@ -57,214 +437,6 @@ export class PhotosUploadHTTPClient { throw e; } } - - /** - * Fetch a fresh list of URLs from remote that can be used to upload files - * and thumbnails to. - * - * @param countHint An approximate number of files that we're expecting to - * upload. - * - * @returns A list of pre-signed object URLs that can be used to upload data - * to the S3 bucket. - */ - async fetchUploadURLs(countHint: number) { - const count = Math.min(50, countHint * 2).toString(); - const params = new URLSearchParams({ count }); - const url = await apiURL("/files/upload-urls"); - const res = await fetch(`${url}?${params.toString()}`, { - headers: await authenticatedRequestHeaders(), - }); - ensureOk(res); - return ObjectUploadURLResponse.parse(await res.json()).urls; - } - - async fetchMultipartUploadURLs( - count: number, - ): Promise { - try { - const response = await HTTPService.get( - await apiURL("/files/multipart-upload-urls"), - { count }, - await authenticatedRequestHeaders(), - ); - - return response.data.urls; - } catch (e) { - log.error("fetch multipart-upload-url failed", e); - throw e; - } - } - - async putFile( - fileUploadURL: ObjectUploadURL, - file: Uint8Array, - progressTracker: unknown, - ): Promise { - try { - await retryAsyncOperation( - () => - HTTPService.put( - fileUploadURL.url, - file, - // @ts-ignore - null, - null, - progressTracker, - ), - handleUploadError, - ); - return fileUploadURL.objectKey; - } catch (e) { - if ( - !( - e instanceof Error && - e.message == CustomError.UPLOAD_CANCELLED - ) - ) { - log.error("putFile to dataStore failed ", e); - } - throw e; - } - } - - async putFileV2( - fileUploadURL: ObjectUploadURL, - file: Uint8Array, - progressTracker: unknown, - ): Promise { - try { - const origin = await uploaderOrigin(); - await retryAsyncOperation(() => - HTTPService.put( - `${origin}/file-upload`, - file, - // @ts-ignore - null, - { "UPLOAD-URL": fileUploadURL.url }, - progressTracker, - ), - ); - return fileUploadURL.objectKey; - } catch (e) { - if ( - !( - e instanceof Error && - e.message == CustomError.UPLOAD_CANCELLED - ) - ) { - log.error("putFile to dataStore failed ", e); - } - throw e; - } - } - - async putFilePart( - partUploadURL: string, - filePart: Uint8Array, - progressTracker: unknown, - ) { - try { - const response = await retryAsyncOperation(async () => { - const resp = await HTTPService.put( - partUploadURL, - filePart, - // @ts-ignore - null, - null, - progressTracker, - ); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (!resp?.headers?.etag) { - const err = Error(CustomError.ETAG_MISSING); - log.error("putFile in parts failed", err); - throw err; - } - return resp; - }, handleUploadError); - return response.headers.etag as string; - } catch (e) { - if ( - !( - e instanceof Error && - e.message == CustomError.UPLOAD_CANCELLED - ) - ) { - log.error("put filePart failed", e); - } - throw e; - } - } - - async putFilePartV2( - partUploadURL: string, - filePart: Uint8Array, - progressTracker: unknown, - ) { - try { - const origin = await uploaderOrigin(); - const response = await retryAsyncOperation(async () => { - const resp = await HTTPService.put( - `${origin}/multipart-upload`, - filePart, - // @ts-ignore - null, - { "UPLOAD-URL": partUploadURL }, - progressTracker, - ); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (!resp?.data?.etag) { - const err = Error(CustomError.ETAG_MISSING); - log.error("putFile in parts failed", err); - throw err; - } - return resp; - }); - return response.data.etag as string; - } catch (e) { - if ( - !( - e instanceof Error && - e.message == CustomError.UPLOAD_CANCELLED - ) - ) { - log.error("put filePart failed", e); - } - throw e; - } - } - - async completeMultipartUpload(completeURL: string, reqBody: unknown) { - try { - await retryAsyncOperation(() => - // @ts-ignore - HTTPService.post(completeURL, reqBody, null, { - "content-type": "text/xml", - }), - ); - } catch (e) { - log.error("put file in parts failed", e); - throw e; - } - } - - async completeMultipartUploadV2(completeURL: string, reqBody: unknown) { - try { - const origin = await uploaderOrigin(); - await retryAsyncOperation(() => - HTTPService.post( - `${origin}/multipart-complete`, - reqBody, - // @ts-ignore - null, - { "content-type": "text/xml", "UPLOAD-URL": completeURL }, - ), - ); - } catch (e) { - log.error("put file in parts failed", e); - throw e; - } - } } /** @@ -287,7 +459,7 @@ export class PublicAlbumsUploadHTTPClient { null, authenticatedPublicAlbumsRequestHeaders(credentials), ), - handleUploadError, + { abortIfNeeded: handleUploadError }, ); return response.data; } catch (e) { @@ -295,38 +467,4 @@ export class PublicAlbumsUploadHTTPClient { throw e; } } - - /** - * Sibling of {@link fetchUploadURLs} for public albums. - */ - async fetchUploadURLs( - countHint: number, - credentials: PublicAlbumsCredentials, - ) { - const count = Math.min(50, countHint * 2).toString(); - const params = new URLSearchParams({ count }); - const url = await apiURL("/public-collection/upload-urls"); - const res = await fetch(`${url}?${params.toString()}`, { - headers: authenticatedPublicAlbumsRequestHeaders(credentials), - }); - ensureOk(res); - return ObjectUploadURLResponse.parse(await res.json()).urls; - } - - async fetchMultipartUploadURLs( - count: number, - credentials: PublicAlbumsCredentials, - ): Promise { - try { - const response = await HTTPService.get( - await apiURL("/public-collection/multipart-upload-urls"), - { count }, - authenticatedPublicAlbumsRequestHeaders(credentials), - ); - return response.data.urls; - } catch (e) { - log.error("fetch public multipart-upload-url failed", e); - throw e; - } - } } diff --git a/web/packages/gallery/services/upload/thumbnail.ts b/web/packages/gallery/services/upload/thumbnail.ts index 49c8b78135..a393397921 100644 --- a/web/packages/gallery/services/upload/thumbnail.ts +++ b/web/packages/gallery/services/upload/thumbnail.ts @@ -2,7 +2,7 @@ import log from "ente-base/log"; import { type Electron } from "ente-base/types/ipc"; import * as ffmpeg from "ente-gallery/services/ffmpeg"; import { - toDataOrPathOrZipEntry, + toPathOrZipEntry, type FileSystemUploadItem, } from "ente-gallery/services/upload"; import { FileType, type FileTypeInfo } from "ente-media/file-type"; @@ -196,7 +196,7 @@ export const generateThumbnailNative = async ( ): Promise => fileTypeInfo.fileType === FileType.image ? await electron.generateImageThumbnail( - toDataOrPathOrZipEntry(fsUploadItem), + toPathOrZipEntry(fsUploadItem), maxThumbnailDimension, maxThumbnailSize, ) diff --git a/web/packages/gallery/services/upload/upload-service.ts b/web/packages/gallery/services/upload/upload-service.ts index 862820df86..9377266374 100644 --- a/web/packages/gallery/services/upload/upload-service.ts +++ b/web/packages/gallery/services/upload/upload-service.ts @@ -6,10 +6,18 @@ import type { BytesOrB64 } from "ente-base/crypto/types"; import { type CryptoWorker } from "ente-base/crypto/worker"; import { ensureElectron } from "ente-base/electron"; import { basename, nameAndExtension } from "ente-base/file-name"; -import type { PublicAlbumsCredentials } from "ente-base/http"; +import { + ensureOk, + retryAsyncOperation, + type HTTPRequestRetrier, + type PublicAlbumsCredentials, +} from "ente-base/http"; import log from "ente-base/log"; import { extractExif } from "ente-gallery/services/exif"; -import { extractVideoMetadata } from "ente-gallery/services/ffmpeg"; +import { + determineVideoDuration, + extractVideoMetadata, +} from "ente-gallery/services/ffmpeg"; import { getNonEmptyMagicMetadataProps, updateMagicMetadata, @@ -37,24 +45,34 @@ import { import { FileType, type FileTypeInfo } from "ente-media/file-type"; import { encodeLivePhoto } from "ente-media/live-photo"; import { addToCollection } from "ente-new/photos/services/collection"; -import { CustomError, handleUploadError } from "ente-shared/error"; +import { settingsSnapshot } from "ente-new/photos/services/settings"; +import { + CustomError, + CustomErrorMessage, + handleUploadError, +} from "ente-shared/error"; import { mergeUint8Arrays } from "ente-utils/array"; import { ensureInteger, ensureNumber } from "ente-utils/ensure"; -import * as convert from "xml-js"; -import type { UploadableUploadItem, UploadItem } from "."; -import { - RANDOM_PERCENTAGE_PROGRESS_FOR_PUT, - type LivePhotoAssets, - type UploadResult, -} from "."; +import type { UploadableUploadItem, UploadItem, UploadPathPrefix } from "."; +import { type LivePhotoAssets, type UploadResult } from "."; import { tryParseEpochMicrosecondsFromFileName } from "./date"; +import { matchJSONMetadata, type ParsedMetadataJSON } from "./metadata-json"; import { + completeMultipartUpload, + completeMultipartUploadViaWorker, + fetchMultipartUploadURLs, + fetchPublicAlbumsMultipartUploadURLs, + fetchPublicAlbumsUploadURLs, + fetchUploadURLs, PhotosUploadHTTPClient, PublicAlbumsUploadHTTPClient, + putFile, + putFilePart, + putFilePartViaWorker, + putFileViaWorker, + type MultipartCompletedPart, type ObjectUploadURL, } from "./remote"; -import type { ParsedMetadataJSON } from "./takeout"; -import { matchTakeoutMetadata } from "./takeout"; import { fallbackThumbnail, generateThumbnailNative, @@ -193,25 +211,23 @@ class UploadService { private async _refillUploadURLs() { let urls: ObjectUploadURL[]; if (this.publicAlbumsCredentials) { - urls = await publicAlbumsHTTPClient.fetchUploadURLs( + urls = await fetchPublicAlbumsUploadURLs( this.pendingUploadCount, this.publicAlbumsCredentials, ); } else { - urls = await photosHTTPClient.fetchUploadURLs( - this.pendingUploadCount, - ); + urls = await fetchUploadURLs(this.pendingUploadCount); } urls.forEach((u) => this.uploadURLs.push(u)); } - async fetchMultipartUploadURLs(count: number) { + async fetchMultipartUploadURLs(uploadPartCount: number) { return this.publicAlbumsCredentials - ? publicAlbumsHTTPClient.fetchMultipartUploadURLs( - count, + ? fetchPublicAlbumsMultipartUploadURLs( + uploadPartCount, this.publicAlbumsCredentials, ) - : photosHTTPClient.fetchMultipartUploadURLs(count); + : fetchMultipartUploadURLs(uploadPartCount); } } @@ -230,19 +246,37 @@ export const uploadItemFileName = (uploadItem: UploadItem) => { return uploadItem.file.name; }; -/* -- Various intermediate type used during upload -- */ +/* -- Various intermediate types used during upload -- */ export type ExternalParsedMetadata = ParsedMetadata & { creationTime?: number | undefined; }; export interface UploadAsset { - /** `true` if this is a live photo. */ + /** + * `true` if this is a live photo. + */ isLivePhoto?: boolean; - /* Valid for live photos */ + /** + * The two parts of the live photo being uploaded. + * + * Valid for live photos. + */ livePhotoAssets?: LivePhotoAssets; - /* Valid for non-live photos */ + /** + * The item being uploaded. + * + * Valid for non-live photos. + */ uploadItem?: UploadItem; + /** + * The path prefix of the uploadItem (if not a live photo), or of the image + * component of the live photo (otherwise). + * + * The only expected scenario where this will not be present is when we're + * uploading an edited file (edited in the in-app image editor). + */ + pathPrefix: UploadPathPrefix | undefined; /** * Metadata we know about a file externally. Valid for non-live photos. * @@ -315,17 +349,12 @@ export interface UploadFile extends BackupedFile { keyDecryptionNonce: string; } -export interface MultipartUploadURLs { - objectKey: string; - partURLs: string[]; - completeURL: string; -} - export interface PotentialLivePhotoAsset { fileName: string; fileType: FileType; collectionID: number; uploadItem: UploadItem; + pathPrefix: UploadPathPrefix | undefined; } /** @@ -337,6 +366,7 @@ export const areLivePhotoAssets = async ( parsedMetadataJSONMap: Map, ) => { if (f.collectionID != g.collectionID) return false; + if (f.pathPrefix != g.pathPrefix) return false; const [fName, fExt] = nameAndExtension(f.fileName); const [gName, gExt] = nameAndExtension(g.fileName); @@ -386,15 +416,16 @@ export const areLivePhotoAssets = async ( // items that coincidentally have the same name (this is not uncommon since, // e.g. many cameras use a deterministic numbering scheme). - const fParsedMetadataJSON = matchTakeoutMetadata( - f.fileName, + const fParsedMetadataJSON = matchJSONMetadata( + f.pathPrefix, f.collectionID, + f.fileName, parsedMetadataJSONMap, ); - - const gParsedMetadataJSON = matchTakeoutMetadata( - g.fileName, + const gParsedMetadataJSON = matchJSONMetadata( + g.pathPrefix, g.collectionID, + g.fileName, parsedMetadataJSONMap, ); @@ -517,17 +548,36 @@ const uploadItemCreationDate = async ( }; /** - * A function that can be called to obtain a "progressTracker" that then is - * directly fed to axios to both cancel the upload if needed, and update the - * progress status. - * - * Enhancement: The return value needs to be typed. + * Some state and callbacks used during upload that are not tied to a specific + * file being uploaded. */ -type MakeProgressTracker = ( - fileLocalID: number, - percentPerPart?: number, - index?: number, -) => unknown; +interface UploadContext { + /** + * If `true`, then the upload does not go via the worker. + * + * See {@link shouldDisableCFUploadProxy} for more details. + */ + isCFUploadProxyDisabled: boolean; + /** + * A function that the upload sequence should use to periodically check in + * and see if the upload has been cancelled by the user. + * + * If the upload has been cancelled, it will throw an exception with the + * message set to {@link CustomError.UPLOAD_CANCELLED}. + */ + abortIfCancelled: () => void; + /** + * A function that gets called update the progress shown in the UI for a + * particular file as the parts of that file get uploaded. + * + * @param {fileLocalID} The local ID of the file whose progress we want to + * update. + * + * @param {percentage} The upload completion percentage, as a value between + * 0 and 100 (inclusive). + */ + updateUploadProgress: (fileLocalID: number, percentage: number) => void; +} interface UploadResponse { uploadResult: UploadResult; @@ -540,6 +590,9 @@ interface UploadResponse { * This is lower layer implementation of the upload. It is invoked by * {@link UploadManager} after it has assembled all the relevant bits we need to * go forth and upload. + * + * @param uploadContext Some general state and callbacks for the entire set of + * files being uploaded. */ export const upload = async ( { collection, localID, fileName, ...uploadAsset }: UploadableUploadItem, @@ -547,10 +600,10 @@ export const upload = async ( existingFiles: EnteFile[], parsedMetadataJSONMap: Map, worker: CryptoWorker, - isCFUploadProxyDisabled: boolean, - abortIfCancelled: () => void, - makeProgressTracker: MakeProgressTracker, + uploadContext: UploadContext, ): Promise => { + const { abortIfCancelled } = uploadContext; + log.info(`Upload ${fileName} | start`); try { /* @@ -583,7 +636,10 @@ export const upload = async ( const { fileTypeInfo, fileSize, lastModifiedMs } = assetDetails; - const maxFileSize = 4 * 1024 * 1024 * 1024; /* 4 GB */ + // TODO(REL): + const maxFileSize = settingsSnapshot().isInternalUser + ? 10 * 1024 * 1024 * 1024 /* 10 GB */ + : 4 * 1024 * 1024 * 1024; /* 4 GB */ if (fileSize >= maxFileSize) return { uploadResult: "tooLarge" }; abortIfCancelled(); @@ -653,11 +709,11 @@ export const upload = async ( const backupedFile = await uploadToBucket( encryptedFilePieces, - makeProgressTracker, - isCFUploadProxyDisabled, - abortIfCancelled, + uploadContext, ); + abortIfCancelled(); + const uploadedFile = await uploadService.uploadFile({ collectionID: collection.id, encryptedKey: encryptedFileKey.encryptedData, @@ -680,7 +736,7 @@ export const upload = async ( const error = handleUploadError(e); switch (error.message) { - case CustomError.ETAG_MISSING: + case CustomErrorMessage.eTagMissing: return { uploadResult: "blocked" }; case CustomError.FILE_TOO_LARGE: return { uploadResult: "largerThanAvailableStorage" }; @@ -909,8 +965,9 @@ const extractAssetMetadata = async ( { isLivePhoto, uploadItem, - externalParsedMetadata, livePhotoAssets, + pathPrefix, + externalParsedMetadata, }: UploadAsset, fileTypeInfo: FileTypeInfo, lastModifiedMs: number, @@ -922,6 +979,7 @@ const extractAssetMetadata = async ( ? await extractLivePhotoMetadata( // @ts-ignore livePhotoAssets, + pathPrefix, fileTypeInfo, lastModifiedMs, collectionID, @@ -931,6 +989,7 @@ const extractAssetMetadata = async ( : await extractImageOrVideoMetadata( // @ts-ignore uploadItem, + pathPrefix, externalParsedMetadata, fileTypeInfo, lastModifiedMs, @@ -941,6 +1000,7 @@ const extractAssetMetadata = async ( const extractLivePhotoMetadata = async ( livePhotoAssets: LivePhotoAssets, + pathPrefix: UploadPathPrefix | undefined, fileTypeInfo: FileTypeInfo, lastModifiedMs: number, collectionID: number, @@ -955,6 +1015,7 @@ const extractLivePhotoMetadata = async ( const { metadata: imageMetadata, publicMagicMetadata } = await extractImageOrVideoMetadata( livePhotoAssets.image, + pathPrefix, undefined, imageFileTypeInfo, lastModifiedMs, @@ -981,6 +1042,7 @@ const extractLivePhotoMetadata = async ( const extractImageOrVideoMetadata = async ( uploadItem: UploadItem, + pathPrefix: UploadPathPrefix | undefined, externalParsedMetadata: ExternalParsedMetadata | undefined, fileTypeInfo: FileTypeInfo, lastModifiedMs: number, @@ -1017,9 +1079,10 @@ const extractImageOrVideoMetadata = async ( // // See: [Note: Duplicate retrieval of creation date for live photo clubbing] - const parsedMetadataJSON = matchTakeoutMetadata( - fileName, + const parsedMetadataJSON = matchJSONMetadata( + pathPrefix, collectionID, + fileName, parsedMetadataJSONMap, ); @@ -1043,6 +1106,12 @@ const extractImageOrVideoMetadata = async ( tryParseEpochMicrosecondsFromFileName(fileName) ?? modificationTime; } + // Video duration + let duration: number | undefined; + if (fileType == FileType.video) { + duration = await tryDetermineVideoDuration(uploadItem); + } + // To avoid introducing malformed data into the metadata fields (which the // other clients might not expect and handle), we have extra "ensure" checks // here that act as a safety valve if somehow the TypeScript type is lying. @@ -1060,6 +1129,10 @@ const extractImageOrVideoMetadata = async ( hash, }; + if (duration) { + metadata.duration = ensureInteger(Math.ceil(duration)); + } + const location = parsedMetadataJSON?.location ?? parsedMetadata?.location; if (location) { metadata.latitude = ensureNumber(location.latitude); @@ -1119,20 +1192,43 @@ const tryExtractVideoMetadata = async (uploadItem: UploadItem) => { } }; +const tryDetermineVideoDuration = async (uploadItem: UploadItem) => { + try { + return await determineVideoDuration(uploadItem); + } catch (e) { + const fileName = uploadItemFileName(uploadItem); + log.error(`Failed to extract video duration for ${fileName}`, e); + return undefined; + } +}; + +/** + * Compute the hash of an item we're attempting to upload. + * + * The hash is retained in the file metadata, and is also used to detect + * duplicates during upload. + * + * This process can take a noticable amount of time. As an extreme case, for a + * 10 GB upload item, this can take a 2-3 minutes. + * + * @param uploadItem The {@link UploadItem} we're attempting to upload. + * + * @param worker A {@link CryptoWorker} to use for computing the hash. + */ const computeHash = async (uploadItem: UploadItem, worker: CryptoWorker) => { const { stream, chunkCount } = await readUploadItem(uploadItem); - const hashState = await worker.initChunkHashing(); + const hashState = await worker.chunkHashInit(); const streamReader = stream.getReader(); for (let i = 0; i < chunkCount; i++) { const { done, value: chunk } = await streamReader.read(); if (done) throw new Error("Less chunks than expected"); - await worker.hashFileChunk(hashState, Uint8Array.from(chunk)); + await worker.chunkHashUpdate(hashState, Uint8Array.from(chunk)); } const { done } = await streamReader.read(); if (!done) throw new Error("More chunks than expected"); - return await worker.completeChunkHashing(hashState); + return await worker.chunkHashFinal(hashState); }; /** @@ -1407,179 +1503,211 @@ const encryptFileStream = async ( const uploadToBucket = async ( encryptedFilePieces: EncryptedFilePieces, - makeProgressTracker: MakeProgressTracker, - isCFUploadProxyDisabled: boolean, - abortIfCancelled: () => void, + uploadContext: UploadContext, ): Promise => { + const { isCFUploadProxyDisabled, abortIfCancelled, updateUploadProgress } = + uploadContext; + const { localID, file, thumbnail, metadata, pubMagicMetadata } = encryptedFilePieces; - try { - let fileObjectKey: string; - let fileSize: number; - const encryptedData = file.encryptedData; - if ( - !(encryptedData instanceof Uint8Array) && - encryptedData.chunkCount >= multipartChunksPerPart - ) { - // We have a stream, and it is more than multipartChunksPerPart - // chunks long, so use a multipart upload to upload it. - ({ objectKey: fileObjectKey, fileSize } = - await uploadStreamUsingMultipart( - localID, - encryptedData, - makeProgressTracker, - isCFUploadProxyDisabled, - abortIfCancelled, - )); - } else { - const data = - encryptedData instanceof Uint8Array - ? encryptedData - : await readEntireStream(encryptedData.stream); - fileSize = data.length; + const requestRetrier = createAbortableRetryEnsuringHTTPOk(abortIfCancelled); - const progressTracker = makeProgressTracker(localID); - const fileUploadURL = await uploadService.getUploadURL(); - if (!isCFUploadProxyDisabled) { - fileObjectKey = await photosHTTPClient.putFileV2( - fileUploadURL, - data, - progressTracker, - ); - } else { - fileObjectKey = await photosHTTPClient.putFile( - fileUploadURL, - data, - progressTracker, - ); - } - } - const thumbnailUploadURL = await uploadService.getUploadURL(); - let thumbnailObjectKey: string; + // The bulk of the network time during upload is taken in uploading the + // actual encrypted objects to remote S3, but after that there is another + // API request we need to make to "finalize" the file (on museum). This + // should be quick usually, but it's a different network route altogether + // and we can't know for sure how long it'll take. So keep aside a small + // approximate percentage for this last step. + const maxPercent = Math.floor(95 + 5 * Math.random()); + + let fileObjectKey: string; + let fileSize: number; + + const encryptedData = file.encryptedData; + if ( + !(encryptedData instanceof Uint8Array) && + encryptedData.chunkCount >= multipartChunksPerPart + ) { + // We have a stream, and it is more than multipartChunksPerPart + // chunks long, so use a multipart upload to upload it. + ({ objectKey: fileObjectKey, fileSize } = + await uploadStreamUsingMultipart( + localID, + encryptedData, + uploadContext, + requestRetrier, + maxPercent, + )); + } else { + const data = + encryptedData instanceof Uint8Array + ? encryptedData + : await readEntireStream(encryptedData.stream); + fileSize = data.length; + + const fileUploadURL = await uploadService.getUploadURL(); + fileObjectKey = fileUploadURL.objectKey; if (!isCFUploadProxyDisabled) { - thumbnailObjectKey = await photosHTTPClient.putFileV2( - thumbnailUploadURL, - thumbnail.encryptedData, - null, - ); + await putFileViaWorker(fileUploadURL.url, data, requestRetrier); } else { - thumbnailObjectKey = await photosHTTPClient.putFile( - thumbnailUploadURL, - thumbnail.encryptedData, - null, - ); + await putFile(fileUploadURL.url, data, requestRetrier); } - - const backupedFile: BackupedFile = { - file: { - decryptionHeader: file.decryptionHeader, - objectKey: fileObjectKey, - size: fileSize, - }, - thumbnail: { - decryptionHeader: thumbnail.decryptionHeader, - objectKey: thumbnailObjectKey, - size: thumbnail.encryptedData.length, - }, - metadata: { - encryptedData: metadata.encryptedDataB64, - decryptionHeader: metadata.decryptionHeaderB64, - }, - pubMagicMetadata: pubMagicMetadata, - }; - return backupedFile; - } catch (e) { - if ( - !(e instanceof Error && e.message == CustomError.UPLOAD_CANCELLED) - ) { - log.error("Error when uploading to bucket", e); - } - throw e; + updateUploadProgress(localID, maxPercent); } + + const thumbnailUploadURL = await uploadService.getUploadURL(); + if (!isCFUploadProxyDisabled) { + await putFileViaWorker( + thumbnailUploadURL.url, + thumbnail.encryptedData, + requestRetrier, + ); + } else { + await putFile( + thumbnailUploadURL.url, + thumbnail.encryptedData, + requestRetrier, + ); + } + + return { + file: { + decryptionHeader: file.decryptionHeader, + objectKey: fileObjectKey, + size: fileSize, + }, + thumbnail: { + decryptionHeader: thumbnail.decryptionHeader, + objectKey: thumbnailUploadURL.objectKey, + size: thumbnail.encryptedData.length, + }, + metadata: { + encryptedData: metadata.encryptedDataB64, + decryptionHeader: metadata.decryptionHeaderB64, + }, + pubMagicMetadata: pubMagicMetadata, + }; }; -interface PartEtag { - PartNumber: number; - ETag: string; -} +/** + * A factory method that returns a function which will act like variant of + * {@link retryEnsuringHTTPOk} and also understands the cancellation mechanism + * used by the upload subsystem. + * + * @param abortIfCancelled A function that aborts the operation by throwing a + * error with the message set to {@link CustomError.UPLOAD_CANCELLED} if the + * user has cancelled the upload. + * + * @return A function of type {@link HTTPRequestRetrier} that can be used to + * retry requests. This function will retry requests (obtained afresh each time + * by calling the provided {@link request} function) in the same manner as + * {@link retryEnsuringHTTPOk}. Additionally, it will call + * {@link abortIfCancelled} before each attempt, and also bypass the retries + * when the abort happens on such cancellations. + */ +const createAbortableRetryEnsuringHTTPOk = + (abortIfCancelled: () => void): HTTPRequestRetrier => + (request, opts) => + retryAsyncOperation( + async () => { + abortIfCancelled(); + const r = await request(); + ensureOk(r); + return r; + }, + { + ...opts, + abortIfNeeded(e) { + if ( + e instanceof Error && + e.message == CustomError.UPLOAD_CANCELLED + ) + throw e; + }, + }, + ); -async function uploadStreamUsingMultipart( +const uploadStreamUsingMultipart = async ( fileLocalID: number, dataStream: EncryptedFileStream, - makeProgressTracker: MakeProgressTracker, - isCFUploadProxyDisabled: boolean, - abortIfCancelled: () => void, -) { + uploadContext: UploadContext, + requestRetrier: HTTPRequestRetrier, + maxPercent: number, +) => { + const { isCFUploadProxyDisabled, abortIfCancelled, updateUploadProgress } = + uploadContext; + const uploadPartCount = Math.ceil( dataStream.chunkCount / multipartChunksPerPart, ); + const multipartUploadURLs = await uploadService.fetchMultipartUploadURLs(uploadPartCount); const { stream } = dataStream; const streamReader = stream.getReader(); - const percentPerPart = - RANDOM_PERCENTAGE_PROGRESS_FOR_PUT() / uploadPartCount; - const partEtags: PartEtag[] = []; + const percentPerPart = maxPercent / uploadPartCount; + let fileSize = 0; + const completedParts: MultipartCompletedPart[] = []; for (const [ index, - fileUploadURL, + partUploadURL, ] of multipartUploadURLs.partURLs.entries()) { abortIfCancelled(); - const uploadChunk = await combineChunksToFormUploadPart(streamReader); - fileSize += uploadChunk.length; - const progressTracker = makeProgressTracker( - fileLocalID, - percentPerPart, - index, - ); - let eTag = null; - if (!isCFUploadProxyDisabled) { - eTag = await photosHTTPClient.putFilePartV2( - fileUploadURL, - uploadChunk, - progressTracker, - ); - } else { - eTag = await photosHTTPClient.putFilePart( - fileUploadURL, - uploadChunk, - progressTracker, - ); - } - partEtags.push({ PartNumber: index + 1, ETag: eTag }); + const partNumber = index + 1; + const partData = await nextMultipartUploadPart(streamReader); + fileSize += partData.length; + + const eTag = !isCFUploadProxyDisabled + ? await putFilePartViaWorker( + partUploadURL, + partData, + requestRetrier, + ) + : await putFilePart(partUploadURL, partData, requestRetrier); + if (!eTag) throw new Error(CustomErrorMessage.eTagMissing); + + updateUploadProgress(fileLocalID, percentPerPart * partNumber); + completedParts.push({ partNumber, eTag }); } const { done } = await streamReader.read(); if (!done) throw new Error("More chunks than expected"); - const completeURL = multipartUploadURLs.completeURL; - const cBody = convert.js2xml( - { CompleteMultipartUpload: { Part: partEtags } }, - { compact: true, ignoreComment: true, spaces: 4 }, - ); + const completionURL = multipartUploadURLs.completeURL; if (!isCFUploadProxyDisabled) { - await photosHTTPClient.completeMultipartUploadV2(completeURL, cBody); + await completeMultipartUploadViaWorker( + completionURL, + completedParts, + requestRetrier, + ); } else { - await photosHTTPClient.completeMultipartUpload(completeURL, cBody); + await completeMultipartUpload( + completionURL, + completedParts, + requestRetrier, + ); } return { objectKey: multipartUploadURLs.objectKey, fileSize }; -} +}; -async function combineChunksToFormUploadPart( +/** + * Construct byte arrays, up to 20 MB each, containing the contents of (up to) + * the next 5 {@link streamEncryptionChunkSize} chunks read from the given + * {@link streamReader}. + */ +const nextMultipartUploadPart = async ( streamReader: ReadableStreamDefaultReader, -) { - const combinedChunks = []; +) => { + const chunks = []; for (let i = 0; i < multipartChunksPerPart; i++) { const { done, value: chunk } = await streamReader.read(); - if (done) { - break; - } - combinedChunks.push(chunk); + if (done) break; + chunks.push(chunk); } - return mergeUint8Arrays(combinedChunks); -} + return mergeUint8Arrays(chunks); +}; diff --git a/web/packages/gallery/services/video.ts b/web/packages/gallery/services/video.ts index b5c5db08c7..726664735e 100644 --- a/web/packages/gallery/services/video.ts +++ b/web/packages/gallery/services/video.ts @@ -1,17 +1,24 @@ import { isDesktop } from "ente-base/app"; import { assertionFailed } from "ente-base/assert"; -import { decryptBlob } from "ente-base/crypto"; +import { decryptBlob, encryptBlobB64 } from "ente-base/crypto"; import type { EncryptedBlob } from "ente-base/crypto/types"; import { ensureElectron } from "ente-base/electron"; import { isHTTP4xxError, type PublicAlbumsCredentials } from "ente-base/http"; +import { getKV, getKVB, getKVN, setKV } from "ente-base/kv"; +import { ensureAuthToken, ensureLocalUser } from "ente-base/local-user"; import log from "ente-base/log"; +import { apiURL } from "ente-base/origins"; import { fileLogID, type EnteFile } from "ente-media/file"; +import { + filePublicMagicMetadata, + updateRemotePublicMagicMetadata, +} from "ente-media/file-metadata"; import { FileType } from "ente-media/file-type"; import { getAllLocalFiles, + getLocalTrashFileIDs, uniqueFilesByID, } from "ente-new/photos/services/files"; -import { settingsSnapshot } from "ente-new/photos/services/settings"; import { gunzip, gzip } from "ente-new/photos/utils/gzip"; import { randomSample } from "ente-utils/array"; import { ensurePrecondition } from "ente-utils/ensure"; @@ -21,12 +28,12 @@ import { initiateGenerateHLS, readVideoStream, videoStreamDone, + type GenerateHLSResult, } from "../utils/native-stream"; import { downloadManager, isNetworkDownloadError } from "./download"; import { fetchFileData, fetchFilePreviewData, - getFilePreviewDataUploadURL, putVideoData, syncUpdatedFileDataFileIDs, } from "./file-data"; @@ -37,6 +44,12 @@ import { type TimestampedFileSystemUploadItem, } from "./upload"; +export type HLSGenerationEnabledStatus = "processing" | "idle"; + +export type HLSGenerationStatus = + | { enabled: false } + | { enabled: true; status?: HLSGenerationEnabledStatus }; + interface VideoProcessingQueueItem { /** * The {@link EnteFile} (guaranteed to be of {@link FileType.video}) whose @@ -63,6 +76,25 @@ const idleWaitMax = idleWaitInitial * 2 ** 6; /* 640 sec */ * This entire object will be reset on logout. */ class VideoState { + /** + * `true` if the generation of HLS streams has been enabled on this client. + */ + isHLSGenerationEnabled = false; + /** + * Subscriptions to {@link HLSGenerationStatus} updates attached using + * {@link hlsGenerationStatusSubscribe}. + */ + hlsGenerationStatusListeners: (() => void)[] = []; + /** + * Snapshot of the {@link HLSGenerationStatus} returned by the + * {@link hlsGenerationStatusSnapshot} function. + */ + hlsGenerationStatusSnapshot: HLSGenerationStatus | undefined; + /** + * Value of the {@link status} field in the last + * {@link hlsGenerationStatusSnapshot}. + */ + lastEnabledStatus: HLSGenerationEnabledStatus | undefined; /** * Queue of recently uploaded items waiting to be processed. */ @@ -117,6 +149,122 @@ export const resetVideoState = () => { _state = new VideoState(); }; +/** + * A function that can be used to subscribe to updates in the HLS generation + * settings and status. + * + * See: [Note: Snapshots and useSyncExternalStore]. + */ +export const hlsGenerationStatusSubscribe = ( + onChange: () => void, +): (() => void) => { + _state.hlsGenerationStatusListeners.push(onChange); + return () => { + _state.hlsGenerationStatusListeners = + _state.hlsGenerationStatusListeners.filter((l) => l != onChange); + }; +}; + +/** + * Return the last know, cached {@link HLSGenerationStatus}. + * + * See also {@link hlsGenerationStatusSubscribe}. + * + * This function can be safely called even if {@link isHLSGenerationSupported} + * is `false` (in such cases, it will always return `undefined`). This is so + * that it can be unconditionally called as part of a React hook. + * + * A return value of `undefined` indicates that the HLS generation subsystem has + * not been initialized yet. + */ +export const hlsGenerationStatusSnapshot = () => + _state.hlsGenerationStatusSnapshot; + +const setHLSGenerationStatusSnapshot = (snapshot: HLSGenerationStatus) => { + _state.hlsGenerationStatusSnapshot = snapshot; + _state.hlsGenerationStatusListeners.forEach((l) => l()); +}; + +/** + * A variant of {@link setHLSGenerationStatusSnapshot} that only triggers an + * update of the snapshot if the enabled state is different from the last known + * enabled state. + */ +const updateSnapshotIfNeeded = ( + status: HLSGenerationEnabledStatus | undefined, +) => { + const enabled = _state.isHLSGenerationEnabled; + if (enabled && status != _state.lastEnabledStatus) { + _state.lastEnabledStatus = status; + setHLSGenerationStatusSnapshot({ enabled, status }); + } +}; + +/** + * Return `true` if this client is capable of generating HLS streams for + * uploaded videos. + */ +export const isHLSGenerationSupported = isDesktop; + +/** + * Initialize the video processing subsystem if the user has enabled HLS + * generation in settings. + */ +export const initVideoProcessing = async () => { + let enabled = false; + if (await savedGenerateHLS()) enabled = true; + + _state.isHLSGenerationEnabled = enabled; + + // Update snapshot to reflect the enabled setting. The status will get + // filled in when we tick. + setHLSGenerationStatusSnapshot({ enabled }); +}; + +/** + * Return the persisted user preference for HLS generation. + */ +const savedGenerateHLS = () => getKVB("generateHLS"); + +/** + * Update the persisted user preference for HLS generation. + * + * Use {@link savedGenerateHLS} to get the persisted value back. + */ +const saveGenerateHLS = (enabled: boolean) => setKV("generateHLS", enabled); + +/** + * Enable or disable (toggle) the HLS generation on this client. + * + * When HLS generation is enabled, this client will process videos to generate a + * streamable variant of them. + * + * Precondition: {@link isHLSGenerationSupported} must be `true`. + */ +export const toggleHLSGeneration = async () => { + if (!isHLSGenerationSupported) { + assertionFailed(); + return; + } + + const enabled = !_state.isHLSGenerationEnabled; + + // Clear transient fields. + _state.lastEnabledStatus = undefined; + + // Update disk. + await saveGenerateHLS(enabled); + // Update in memory. + _state.isHLSGenerationEnabled = enabled; + + // Update snapshot. Right now we only set the enabled setting. The status + // will get filled in when we tick. + setHLSGenerationStatusSnapshot({ enabled }); + + // Wake up the processor if needed. + if (enabled) tickNow(); +}; + export interface HLSPlaylistData { /** A data URL to a HLS playlist that streams the video. */ playlistURL: string; @@ -126,6 +274,11 @@ export interface HLSPlaylistData { height: number; } +/** + * See: [Note: Caching HLS playlist data] for the semantics of "skip". + */ +export type HLSPlaylistDataForFile = HLSPlaylistData | "skip" | undefined; + /** * Return a HLS playlist that can be used to stream playback of then given video * {@link file}. @@ -151,7 +304,7 @@ export interface HLSPlaylistData { * - If a file has a corresponding HLS playlist, then currently there is no * scenario (apart from file deletion, where the playlist also gets deleted) * where the playlist is deleted after being created. There is a limit to the - * validity of the presigned chunk URLs within the playlist we create (which + * validity of the pre-signed chunk URLs within the playlist we create (which * we do handle, see `createHLSPlaylistItemDataValidity`), but the original * playlist itself does not change. Updates are technically possible, but * apart from a misbehaving client, are not expected (and should be no-ops in @@ -159,34 +312,35 @@ export interface HLSPlaylistData { * regenerated again). All in all, this means that a positive result ("this * file has a playlist") can be cached indefinitely. * - * - If a file does not have a HLS playlist, and it is eligible for being - * streamed (e.g. it is not too small where the streaming overhead is not - * required), then a client (this one, or a different one) can process it at - * any arbitrary time. So the negative result ("this file does not have a + * - If a file does not have a HLS playlist, it might be because it is not + * eligible for being streamed (e.g. it is already small and in a compatible + * codec). See [Note: Marking files which do not need video processing] for + * more details of this case. In particular, we can cache this state in memory + * indefinitely too, since there isn't a current case where either this + * eligibility can change, or the client gain the ability to handle them + * without restarting. + * + * - Finally, if a file does not have an HLS playlist but and it is eligible for + * being streamed, then a client (this one, or a different one) can process it + * at any arbitrary time. So the negative result ("this file does not have a * playlist") cannot be cached. * - * So while we can easily cache the first case ("this file has a playlist"), we - * need to deal with the second case ("this file does not have a playlist") a - * bit more intricately: - * - * - If running in the context of a logged in user (e.g. photos app), we can use - * the "/files/data/status-diff" API to be notified of any modifications to - * the second case for the user's own files. This status-diff happens during - * the regular "sync", and we can use that as a cue to selectively prune cache - * entries for the second case (but can otherwise indefinitely cache it). - * - * - If the file is a shared file, the status-diff will not return it. And if - * we're not running in the context of a logged in user (e.g. the public - * albums app), then there is no status-diff to do. For these two scenarios, - * we thus mark the cached values as "transient" and always recheck for a - * playlist when opening the slide. + * So while we can easily cache the first case ("this file has a playlist") and + * second case ("this file doesn't need a streaming variant"), we need to deal + * with the third case ("this file does not have a playlist") by marking the + * cached values as "transient" and always recheck for a playlist when opening + * the slide. */ export const hlsPlaylistDataForFile = async ( file: EnteFile, publicAlbumsCredentials?: PublicAlbumsCredentials, -): Promise => { +): Promise => { ensurePrecondition(file.metadata.fileType == FileType.video); + if (filePublicMagicMetadata(file)?.sv == 1) { + return "skip"; + } + const playlistFileData = await fetchFileData( "vid_preview", file.id, @@ -351,11 +505,6 @@ const blobToDataURL = (blob: Blob) => reader.readAsDataURL(blob); }); -// TODO(HLS): Store this in DB. -let _videoPreviewProcessedFileIDs: number[] = []; -let _videoPreviewFailedFileIDs: number[] = []; -let _videoPreviewSyncLastUpdatedAt: number | undefined; - /** * Return the (persistent) {@link Set} containing the ids of the files which * have already been processed for generating their streaming variant. @@ -368,11 +517,18 @@ let _videoPreviewSyncLastUpdatedAt: number | undefined; * The data is retrieved from persistent storage (KV DB), where it is stored as * an array. */ -const savedProcessedVideoFileIDs = async () => { - // TODO(HLS): make async - await wait(0); - return new Set(_videoPreviewProcessedFileIDs); -}; +const savedProcessedVideoFileIDs = () => + // [Note: Avoiding zod parsing overhead for DB arrays] + // + // Validating that the value we read from the DB is indeed the same as the + // type we expect can be done using zod, but for potentially very large + // arrays, this has an overhead that is perhaps not justified when dealing + // with DB entries we ourselves wrote. + // + // As an optimization, we skip the runtime check here and cast. This might + // not be the most optimal choice in the future, so (a) use it sparingly, + // and (b) mark all such cases with the title of this note. + getKV("videoPreviewProcessedFileIDs").then((v) => new Set(v as number[])); /** * Return the (persistent) {@link Set} containing the ids of the files for which @@ -380,11 +536,9 @@ const savedProcessedVideoFileIDs = async () => { * * @see also {@link savedProcessedVideoFileIDs}. */ -const savedFailedVideoFileIDs = async () => { - // TODO(HLS): make async - await wait(0); - return new Set(_videoPreviewFailedFileIDs); -}; +const savedFailedVideoFileIDs = () => + // See: [Note: Avoiding zod parsing overhead for DB arrays] + getKV("videoPreviewFailedFileIDs").then((v) => new Set(v as number[])); /** * Update the persisted set of IDs of files which have already been processed @@ -392,11 +546,8 @@ const savedFailedVideoFileIDs = async () => { * * @see also {@link savedProcessedVideoFileIDs}. */ -const saveProcessedVideoFileIDs = async (videoFileIDs: Set) => { - // TODO(HLS): make async - await wait(0); - _videoPreviewProcessedFileIDs = Array.from(videoFileIDs); -}; +const saveProcessedVideoFileIDs = (videoFileIDs: Set) => + setKV("videoPreviewProcessedFileIDs", Array.from(videoFileIDs)); /** * Update the persisted set of IDs of files for which attempt to generate a @@ -404,11 +555,8 @@ const saveProcessedVideoFileIDs = async (videoFileIDs: Set) => { * * @see also {@link savedProcessedVideoFileIDs}. */ -const saveFailedVideoFileIDs = async (videoFileIDs: Set) => { - // TODO(HLS): make async - await wait(0); - _videoPreviewFailedFileIDs = Array.from(videoFileIDs); -}; +const saveFailedVideoFileIDs = (videoFileIDs: Set) => + setKV("videoPreviewFailedFileIDs", Array.from(videoFileIDs)); /** * Mark the provided file ID as having been processed to generate a video @@ -441,7 +589,7 @@ const markProcessedVideoFileIDs = async (fileIDs: Set) => { }; /** - * Mark the provided file ID as having failed in a non-transient manner when we + * Mark the provided file as having failed in a non-transient manner when we * tried processing it to generate a video preview on this client. * * Similar to [Note: Transient and permanent indexing failures], we attempt to @@ -454,9 +602,10 @@ const markProcessedVideoFileIDs = async (fileIDs: Set) => { * The mark is local only, and will be reset on logout, or if another client * with a different able is able to process it. */ -const markFailedVideoFileID = async (fileID: number) => { +const markFailedVideoFile = async (file: EnteFile) => { + log.info(`Generate HLS for ${fileLogID(file)} | failed`); const failedIDs = await savedFailedVideoFileIDs(); - failedIDs.add(fileID); + failedIDs.add(file.id); await saveFailedVideoFileIDs(failedIDs); }; @@ -466,11 +615,7 @@ const markFailedVideoFileID = async (fileID: number) => { * The returned value is an epoch millisecond value suitable to be passed to * {@link syncUpdatedFileDataFileIDs}. */ -const savedSyncLastUpdatedAt = async () => { - // TODO(HLS): make async - await wait(0); - return _videoPreviewSyncLastUpdatedAt; -}; +const savedSyncLastUpdatedAt = () => getKVN("videoPreviewSyncLastUpdatedAt"); /** * Update the persisted timestamp used for syncing processed file IDs with @@ -478,11 +623,8 @@ const savedSyncLastUpdatedAt = async () => { * * Use {@link savedSyncLastUpdatedAt} to get the persisted value back. */ -const saveSyncLastUpdatedAt = async (lastUpdatedAt: number) => { - // TODO(HLS): make async - await wait(0); - _videoPreviewSyncLastUpdatedAt = lastUpdatedAt; -}; +const saveSyncLastUpdatedAt = (lastUpdatedAt: number) => + setKV("videoPreviewSyncLastUpdatedAt", lastUpdatedAt); /** * Fetch IDs of files from remote that have been processed by other clients @@ -500,6 +642,33 @@ const syncProcessedFileIDs = async () => }, ); +/** + * Remove any saved entries for file IDs which were previously in trash but now + * have been permanently deleted. + * + * This is called when processing the trash diff. It gives us a hook to clear + * these IDs from our video processing related local state. + * + * See: [Note: Pruning stale status-diff entries] + * + * It is a no-op when we're running in the context of the web app (since it + * doesn't currently process videos, so doesn't need to keep any local state for + * that purpose). + */ +export const videoPrunePermanentlyDeletedFileIDsIfNeeded = async ( + deletedFileIDs: Set, +) => { + if (!isHLSGenerationSupported) return; + + const existing = await savedProcessedVideoFileIDs(); + if (existing.size > 0) { + const updated = existing.difference(deletedFileIDs); + if (updated.size != existing.size) { + await saveProcessedVideoFileIDs(updated); + } + } +}; + /** * If video processing is enabled, trigger a sync with remote and any subsequent * backfill queue processing for pending videos. @@ -518,11 +687,18 @@ const syncProcessedFileIDs = async () => * that have already been processed elsewhere. */ export const videoProcessingSyncIfNeeded = async () => { - if (!isDesktop) return; - if (!isVideoProcessingEnabled()) return; + if (!isHLSGenerationSupported) return; + + // The `haveSyncedOnce` flag tracks whether or not a sync has happened for + // the app, and is not specific to video processing. We always set it even + // if HLS generation is currently disabled so that we can immediately start + // processing the backfill if it gets video processing gets enabled during + // the app's session, without waiting for the next sync to happen. + _state.haveSyncedOnce = true; + + if (!isHLSGenerationEnabled()) return; await syncProcessedFileIDs(); - _state.haveSyncedOnce = true; tickNow(); /* if not already ticking */ }; @@ -554,8 +730,8 @@ export const processVideoNewUpload = ( file: EnteFile, processableUploadItem: ProcessableUploadItem, ) => { - if (!isDesktop) return; - if (!isVideoProcessingEnabled()) return; + if (!isHLSGenerationSupported) return; + if (!isHLSGenerationEnabled()) return; if (file.metadata.fileType !== FileType.video) return; if (processableUploadItem instanceof File) { // While the types don't guarantee it, we really shouldn't be getting @@ -600,10 +776,7 @@ const tickNow = () => { _state.queueProcessor ??= processQueue(); }; -export const isVideoProcessingEnabled = () => - // TODO(HLS): - process.env.NEXT_PUBLIC_ENTE_WIP_VIDEO_STREAMING && - settingsSnapshot().isInternalUser; +export const isHLSGenerationEnabled = () => _state.isHLSGenerationEnabled; /** * The video processing loop goes through videos one by one, preferring items in @@ -642,51 +815,52 @@ export const isVideoProcessingEnabled = () => * batches, and the externally triggered processing of live uploads. */ const processQueue = async () => { - if (!(isDesktop && isVideoProcessingEnabled())) { + if (!isHLSGenerationSupported || !isHLSGenerationEnabled()) { assertionFailed(); /* we shouldn't have come here */ return; } + const userID = ensureLocalUser().id; + + // We mark failures in the local DB for in expected failure mode. As an + // additional protection against loops in unforeseen scenarios, keep a + // transient in-memory list of IDs which shouldn't be looped. + const transientFailedFileIDs = new Set(); + let bq: typeof _state.liveQueue | undefined; - while (isVideoProcessingEnabled()) { + while (isHLSGenerationEnabled()) { let item = _state.liveQueue.shift(); if (!item) { - if (!bq && _state.haveSyncedOnce) { - /* initialize */ - bq = await backfillQueue(); - } - if (bq) { - switch (bq.length) { - case 0: - /* no more items to backfill */ - break; - case 1 /* last item. take it, and refill queue */: - item = bq.pop(); - bq = await backfillQueue(); - break; - default: - /* more than one item. take it */ - item = bq.pop(); - break; + // Initialize or refill queue. + if (!bq?.length) { + if (_state.haveSyncedOnce) { + bq = await backfillQueue(userID); + } else { + log.info("Not attempting backfill until first sync"); } - } else { - log.info("Not backfilling since we haven't synced yet"); } + // Take item if queue is not empty. + if (bq?.length) item = bq.pop(); } - if (item) { + if (item && !transientFailedFileIDs.has(item.file.id)) { + updateSnapshotIfNeeded("processing"); + try { await processQueueItem(item); await markProcessedVideoFileID(item.file.id); + // Reset the idle wait on success. + _state.idleWait = idleWaitInitial; } catch (e) { // This will get retried again at some point later. log.error(`Failed to process video ${fileLogID(item.file)}`, e); + transientFailedFileIDs.add(item.file.id); } - // Reset the idle wait on any activity. - _state.idleWait = idleWaitInitial; } else { // There are no more items in either the live queue or backlog. // Go to sleep (for increasingly longer durations, capped at a // maximum). + updateSnapshotIfNeeded("idle"); + const idleWait = _state.idleWait; _state.idleWait = Math.min(idleWait * 2, idleWaitMax); @@ -700,6 +874,8 @@ const processQueue = async () => { } } + updateSnapshotIfNeeded(undefined); + _state.queueProcessor = undefined; }; @@ -707,16 +883,34 @@ const processQueue = async () => { * Return the next batch of videos that need to be processed. * * If there is nothing pending, return an empty array. + * + * @param userID The ID of the currently logged in user. This is used to filter + * the files to only include those that are owned by the user. */ -const backfillQueue = async (): Promise => { +const backfillQueue = async ( + userID: number, +): Promise => { const allCollectionFiles = await getAllLocalFiles(); + const localTrashFileIDs = await getLocalTrashFileIDs(); + const videoFiles = uniqueFilesByID( + allCollectionFiles.filter( + (f) => + // Only files the user owns. + f.ownerID == userID && + // Only videos. + f.metadata.fileType == FileType.video && + // Not in trash. + !localTrashFileIDs.has(f.id) && + // See: [Note: Marking files which do not need video processing] + filePublicMagicMetadata(f)?.sv != 1, + ), + ); + const doneIDs = (await savedProcessedVideoFileIDs()).union( await savedFailedVideoFileIDs(), ); - const videoFiles = uniqueFilesByID( - allCollectionFiles.filter((f) => f.metadata.fileType == FileType.video), - ); const pendingVideoFiles = videoFiles.filter((f) => !doneIDs.has(f.id)); + const batch = randomSample(pendingVideoFiles, 50); return batch.map((file) => ({ file })); }; @@ -771,10 +965,11 @@ const processQueueItem = async ({ uploadItem; if (!sourceVideo) { try { - sourceVideo = (await downloadManager.fileStream(file))!; + sourceVideo = (await downloadManager.fileStream(file, { + background: true, + }))!; } catch (e) { - if (!isNetworkDownloadError(e)) - await markFailedVideoFileID(file.id); + if (!isNetworkDownloadError(e)) await markFailedVideoFile(file); throw e; } } @@ -791,26 +986,64 @@ const processQueueItem = async ({ // duplicate the stream beforehand, which invalidates the point of // streaming. // - // So instead we provide the presigned upload URL to the node side so that - // it can directly upload the generated video segments. - const { objectID, url: objectUploadURL } = - await getFilePreviewDataUploadURL(file); + // Another mid-way option was to do it partially here - obtain the pre-signed + // upload URLs here (since we already have the rest of the scaffolding to + // make API requests), and then provide this pre-signed URL to the node side + // so that it can directly upload the generated video segments. + // + // However, that then gets into a issue for multipart uploads since we don't + // know the size of the generated HLS video segment file beforehand. We can + // try to estimate it, and that is indeed what we started off with, and that + // approach worked fine too. + // + // However, estimates being estimates, it felt better to make things more + // deterministic by moving the request for the pre-signed URLs also to the + // desktop app side. This also sidesteps the issue of passing along too much + // data (the multipart upload URLs) as request params to the desktop app. + // There was no specific issue again, it just felt that doing everything in + // the desktop app is more simple and straightforward (at the cost of + // needing set up of some API request scaffolding on the desktop side). + // + // Below we prepare the things that we need to pass to the desktop app to + // allow it to make the API request for obtaining pre-signed upload URLs. + const fetchURL = await apiURL("/files/data/preview-upload-url"); + const authToken = await ensureAuthToken(); log.info(`Generate HLS for ${fileLogID(file)} | start`); - // TODO(HLS): Inside this needs to be more granular in case of errors. - const res = await initiateGenerateHLS( - electron, - sourceVideo, - objectUploadURL, - ); + let res: GenerateHLSResult | undefined; + try { + res = await initiateGenerateHLS( + electron, + sourceVideo, + file.id, + fetchURL, + authToken, + ); + } catch (e) { + // Failures during stream generation on the native side are expected to + // happen in two cases: + // + // 1. There is something specific to this video that doesn't work with + // the current HLS generation pipeline (the ffmpeg invocation). + // + // 2. The upload of the generated video fails. + // + // The native side code already retries failures for case 2 (except HTTP + // 4xx errors). Thus, usually we should come here only for case 1, and + // retrying the same video again will not work either. + await markFailedVideoFile(file); + throw e; + } if (!res) { log.info(`Generate HLS for ${fileLogID(file)} | not-required`); + // See: [Note: Marking files which do not need video processing] + await updateRemotePublicMagicMetadata(file, { sv: 1 }); return; } - const { playlistToken, dimensions, videoSize } = res; + const { playlistToken, dimensions, videoSize, videoObjectID } = res; try { const playlist = await readVideoStream(electron, playlistToken).then( (res) => res.text(), @@ -823,10 +1056,17 @@ const processQueueItem = async ({ size: videoSize, }); + const encryptedPlaylist = await encryptBlobB64(playlistData, file.key); + try { - await putVideoData(file, playlistData, objectID, videoSize); + await putVideoData( + file, + encryptedPlaylist, + videoObjectID, + videoSize, + ); } catch (e) { - if (isHTTP4xxError(e)) await markFailedVideoFileID(file.id); + if (isHTTP4xxError(e)) await markFailedVideoFile(file); throw e; } diff --git a/web/packages/gallery/utils/native-stream.ts b/web/packages/gallery/utils/native-stream.ts index 62649f342f..7863f0d33c 100644 --- a/web/packages/gallery/utils/native-stream.ts +++ b/web/packages/gallery/utils/native-stream.ts @@ -105,11 +105,16 @@ export const writeStream = async ( const req = new Request(url, { // GET can't have a body method: "POST", - body: stream, - // @ts-expect-error TypeScript's libdom.d.ts does not include the - // "duplex" parameter, e.g. see + // [Note: duplex param required for stream body] + // + // The duplex parameter needs to be set to 'half' when streaming + // requests. However, TypeScript's libdom.d.ts does not include the + // "duplex" parameter, so we need to silence the tsc error. e.g. see // https://github.com/node-fetch/node-fetch/issues/1769. + // + // @ts-expect-error See: [Note: duplex param required for stream body] duplex: "half", + body: stream, }); const res = await fetch(req); @@ -160,6 +165,10 @@ const GenerateHLSResult = z.object({ * The size (in bytes) of the file containing the encrypted video segments. */ videoSize: z.number(), + /** + * The ID of the uploaded encrypted video segment file on the remote bucket. + */ + videoObjectID: z.string(), }); export type GenerateHLSResult = z.infer; @@ -184,12 +193,17 @@ export type GenerateHLSResult = z.infer; * * - Otherwise it should be a {@link ReadableStream} of the video contents. * - * @param objectUploadURL A presigned URL where the video segments should be - * uploaded to. + * @param fileID The ID of the {@link EnteFile} for which we're generating the + * HLS. + * + * @param fetchURL The fully resolved API URL for obtaining the pre-signed URLs + * to which the video segment file should be uploaded. + * + * @param authToken The user's auth token (for making the request to + * {@link fetchURL}). * * @returns a token that can be used to retrieve the generated HLS playlist, and - * metadata about the generated video (its byte size and dimensions). See {@link - * GenerateHLSResult}. + * metadata about the generated video (See {@link GenerateHLSResult}). * * In case the video is such that it doesn't require a separate stream to be * generated (e.g. it is a small video using an already compatible codec), then @@ -200,9 +214,16 @@ export type GenerateHLSResult = z.infer; export const initiateGenerateHLS = async ( _: Electron, video: FileSystemUploadItem | ReadableStream, - objectUploadURL: string, + fileID: number, + fetchURL: string, + authToken: string, ): Promise => { - const params = new URLSearchParams({ op: "generate-hls", objectUploadURL }); + const params = new URLSearchParams({ + op: "generate-hls", + fileID: fileID.toString(), + fetchURL, + authToken, + }); let body: ReadableStream | null; if (video instanceof ReadableStream) { @@ -227,11 +248,7 @@ export const initiateGenerateHLS = async ( const url = `stream://video?${params.toString()}`; const res = await fetch(url, { method: "POST", - // The duplex option is required when body is a stream. - // - // @ts-expect-error TypeScript's libdom.d.ts does not include the - // "duplex" parameter, e.g. see - // https://github.com/node-fetch/node-fetch/issues/1769. + // @ts-expect-error See: [Note: duplex param required for stream body] duplex: "half", body, }); @@ -246,7 +263,8 @@ export const initiateGenerateHLS = async ( /** * Variant of {@link readStream} tailored for video conversion. * - * @param token A token obtained from {@link writeVideoStream}. + * @param token A token obtained from a video conversion operation like + * {@link initiateConvertToMP4} or {@link initiateGenerateHLS}. * * @returns a Response that contains the data associated with the provided * token. @@ -270,10 +288,8 @@ export const readVideoStream = async ( }; /** - * Sibling of {@link readConvertToMP4Stream} to let the native side know when we - * are done reading the response, so it can dispose any temporary resources. - * - * @param token A token obtained from {@link writeVideoStream}. + * Sibling of {@link readVideoStream} to let the native side know when we are + * done reading the response, so it can dispose any temporary resources. */ export const videoStreamDone = async ( _: Electron, diff --git a/web/packages/media/collection.ts b/web/packages/media/collection.ts index cf078880db..5581cac124 100644 --- a/web/packages/media/collection.ts +++ b/web/packages/media/collection.ts @@ -6,14 +6,101 @@ import { ItemVisibility } from "ente-media/file-metadata"; // TODO: Audit this file -export type CollectionType = "folder" | "favorites" | "album" | "uncategorized"; +/** + * The type of a collection. + * + * - "album" - A regular "Ente Album" that the user sees in their library. + * + * - "folder" - An Ente Album that is also associated with an OS album on the + * user's mobile device. + * + * A collection of type "folder" is created by the mobile app if there is an + * associated on-device album for the new Ente album being created. + * + * This separation between "album" and "folder" allows different mobile + * clients to push to the same Folder ("Camera", "Screenshots"), not allowing + * for duplicate folders with the same name, while still allowing users to + * create different albums with the same name. + * + * The web/desktop app does not create collections of type "folder", and + * otherwise treats them as aliases for "album". + * + * - "favorites" - A special collection consisting of the items that the user + * has marked as their favorites. + * + * The user can have at most one collection of type "favorites" (enforced at + * remote). This collection is created on demand by the client where the user + * first marks an item as a favorite. The user can choose to share their + * "favorites" with other users, so it is possible for there to be multiple + * collections of type "favorites" present in our local database, however only + * one of those will belong to the logged in user (cf `owner.id`). + * + * - "uncategorized" - A special collection consisting of items that do not + * belong to any other collection. + * + * In the remote schema, each item ({@link EnteFile}) is always associated + * with a collection. The same item may belong to multiple collections (See: + * [Note: Collection File]), but it must belong to at least one collection. + * + * In some scenarios, e.g. when deleting the last collection to which a file + * belongs, the file would thus get orphaned and violate the schema + * invariants. So in such cases, the client which is performing the + * corresponding operation moves the file to the user's special + * "uncategorized" collection, creating it if needed. + * + * Similar to "favorites", the user can have only one "uncategorized" + * collection. However, unlike "favorites", the "uncategorized" collection + * cannot be shared. + */ +export type CollectionType = "album" | "folder" | "favorites" | "uncategorized"; -export type CollectionRole = "VIEWER" | "OWNER" | "COLLABORATOR" | "UNKNOWN"; +/** + * The privilege level of a participant associated with a collection. + * + * - "VIEWER" - Has read-only access to files in the collection. + * + * - "COLLABORATOR" - Can additionally add files from the collection, and remove + * files that they added from the collection (i.e., files they "own"). + * + * - "OWNER" - The owner of the collection. Can remove any file, including those + * added by other users, from the collection. + * + * It is guaranteed that a there will be exactly one participant of type OWNER, + * and their user ID will be the same as the collection `owner.id`. + */ +export type CollectionUserRole = + | "VIEWER" + | "COLLABORATOR" + | "OWNER" + | "UNKNOWN"; +/** + * Information about the user associated with a collection, either as an owner, + * or as someone with whom the collection has been shared with. + */ export interface CollectionUser { + /** + * The ID of the underlying {@link User} that this {@link CollectionUser} + * stands for. + */ id: number; - email: string; - role: CollectionRole; + /** + * The email of the user. + * + * - The email is present for the {@link owner} only for shared collections. + * - The email is present for all {@link sharees}. + * - Remote uses a blank string to indicate absent values. + */ + email?: string; + /** + * The association / privilege level of the user with the collection. + * + * - The role is not present blank for the {@link owner}. + * - The role is present, and one of "VIEWER" and "COLLABORATOR" for the + * {@link sharees}. + * - Remote uses a blank string to indicate absent values. + */ + role?: CollectionUserRole; } export interface EncryptedCollection { @@ -25,6 +112,13 @@ export interface EncryptedCollection { * all collections on an Ente instance (i.e., it is not scoped to a user). */ id: number; + /** + * Information about the user who owns the collection. + * + * Each collection is owned by exactly one user. The owner may optionally + * choose to share it with additional users, granting them varying level of + * privileges. + */ owner: CollectionUser; // collection name was unencrypted in the past, so we need to keep it as optional name?: string; diff --git a/web/packages/media/file-metadata.ts b/web/packages/media/file-metadata.ts index 4fcda2e238..0056e4928f 100644 --- a/web/packages/media/file-metadata.ts +++ b/web/packages/media/file-metadata.ts @@ -140,6 +140,14 @@ export interface Metadata { * older clients. */ videoHash?: string; + /** + * The duration (in integral seconds) of the video. + * + * Only present for videos (`fileType == FileType.video`). For compatibility + * with other clients, this must be a integer number of seconds, without any + * sub-second fraction. + */ + duration?: number; hasStaticThumbnail?: boolean; localID?: number; version?: number; @@ -274,6 +282,33 @@ export interface PublicMagicMetadata { */ caption?: string; uploaderName?: string; + /** + * An arbitrary integer set to indicate that this file should be skipped for + * the purpose of HLS generation. + * + * Current semantics: + * + * - if 1, skip this file + * - otherwise attempt processing + * + * [Note: Marking files which do not need video processing] + * + * Some video files do not require generation of a HLS stream. The current + * logic is H.264 files less than 10 MB, but this might change in future + * clients. + * + * For such skipped files, there thus won't be a HLS playlist generated. + * However, we still need a way to indicate to other clients that this file + * has already been looked at. + * + * To that end, we add a flag to the public magic metadata for the file. To + * allow future flexibility, this flag is an integer "streaming version". + * Currently it is set to 1 by a client who recognizes that this file does + * not need processing, and other clients can ignore this file if they find + * sv == 1. In the future, there might be other values for sv (e.g. if the + * skip logic changes). + */ + sv?: number; } /** @@ -732,6 +767,44 @@ export const fileLocation = (file: EnteFile): Location | undefined => { return { latitude, longitude }; }; +/** + * Return the duration of the video as a formatted "HH:mm:ss" string (when + * present) for the given {@link EnteFile}. + * + * Only files with type `FileType.video` are expected to have a duration. + * + * @returns The duration of the video as a string of the form "HH:mm:ss". The + * underlying duration present in the file's metadata is guaranteed to be + * integral, so there will never be a subsecond component. + * + * - If the hour component is all zeroes, it will be omitted. + * + * - Leading zeros in the minutes component will be trimmed off if an hour + * component is not present. If minutes is all zeros, then "0" will be used. + * + * - For example, an underlying duration of 595 seconds will result in a + * formatted string of the form "9:55". While an underlying duration of 9 + * seconds will be returned as a string "0:09". + * + * - A zero duration will be treated as undefined. + */ +export const fileDurationString = (file: EnteFile): string | undefined => { + const d = file.metadata.duration; + if (!d) return undefined; + + const s = d % 60; + const m = Math.floor(d / 60) % 60; + const h = Math.floor(d / 3600); + + const ss = s > 9 ? `${s}` : `0${s}`; + if (h) { + const mm = m > 9 ? `${m}` : `0${m}`; + return `${h}:${mm}:${ss}`; + } else { + return `${m}:${ss}`; + } +}; + /** * Return the caption, aka "description", (if any) attached to the given * {@link EnteFile}. diff --git a/web/packages/new/package.json b/web/packages/new/package.json index 487a303c43..b4dff01d1f 100644 --- a/web/packages/new/package.json +++ b/web/packages/new/package.json @@ -5,7 +5,7 @@ "dependencies": { "@mui/material": "^6.4.11", "@mui/system": "^6.4.11", - "@mui/x-date-pickers": "^7.29.2", + "@mui/x-date-pickers": "^7.29.3", "dayjs": "^1.11.13", "ente-base": "*", "ente-gallery": "*", @@ -15,8 +15,8 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.3", + "@types/react": "^19.1.5", + "@types/react-dom": "^19.1.5", "ente-build-config": "*" } } diff --git a/web/packages/new/photos/components/Export.tsx b/web/packages/new/photos/components/Export.tsx new file mode 100644 index 0000000000..ed5b79a834 --- /dev/null +++ b/web/packages/new/photos/components/Export.tsx @@ -0,0 +1,635 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ +import FolderIcon from "@mui/icons-material/Folder"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + LinearProgress, + Stack, + Tooltip, + Typography, +} from "@mui/material"; +import { isDesktop } from "ente-base/app"; +import { EnteSwitch } from "ente-base/components/EnteSwitch"; +import { LinkButton } from "ente-base/components/LinkButton"; +import { TitledMiniDialog } from "ente-base/components/MiniDialog"; +import { + OverflowMenu, + OverflowMenuOption, +} from "ente-base/components/OverflowMenu"; +import { EllipsizedTypography } from "ente-base/components/Typography"; +import { SpacedRow } from "ente-base/components/containers"; +import type { ButtonishProps } from "ente-base/components/mui"; +import { DialogCloseIconButton } from "ente-base/components/mui/DialogCloseIconButton"; +import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; +import { + useModalVisibility, + type ModalVisibilityProps, +} from "ente-base/components/utils/modal"; +import { useBaseContext } from "ente-base/context"; +import { ensureElectron } from "ente-base/electron"; +import { formattedNumber } from "ente-base/i18n"; +import { formattedDateTime } from "ente-base/i18n-date"; +import log from "ente-base/log"; +import { type EnteFile } from "ente-media/file"; +import { ItemCard, PreviewItemTile } from "ente-new/photos/components/Tiles"; +import { CustomError } from "ente-shared/error"; +import { t } from "i18next"; +import React, { memo, useCallback, useEffect, useState } from "react"; +import { Trans } from "react-i18next"; +import { + areEqual, + FixedSizeList, + type ListChildComponentProps, + type ListItemKeySelector, +} from "react-window"; +import exportService, { + ExportStage, + selectAndPrepareExportDirectory, + type ExportOpts, + type ExportProgress, + type ExportSettings, +} from "../services/export"; + +type ExportProps = ModalVisibilityProps & { + /** + * A map from collection IDs to their user visible name. + * + * It will contain entries for all collections (both normal and hidden). + */ + collectionNameByID: Map; +}; + +/** + * A dialog that allows the user to view and manage the export of their data. + * + * Available only in the desktop app (export requires direct disk access). + */ +export const Export: React.FC = ({ + open, + onClose, + collectionNameByID, +}) => { + const { showMiniDialog } = useBaseContext(); + const [exportStage, setExportStage] = useState( + ExportStage.init, + ); + const [exportFolder, setExportFolder] = useState(""); + const [continuousExport, setContinuousExport] = useState(false); + const [exportProgress, setExportProgress] = useState({ + success: 0, + failed: 0, + total: 0, + }); + // The list of EnteFiles that have not been exported yet. + const [pendingFiles, setPendingFiles] = useState([]); + const [lastExportTime, setLastExportTime] = useState(0); + + const syncExportRecord = useCallback(async (exportFolder: string) => { + try { + if (!(await exportService.exportFolderExists(exportFolder))) { + setPendingFiles(await exportService.pendingFiles()); + } + const exportRecord = + await exportService.getExportRecord(exportFolder); + setExportStage(exportRecord.stage); + setLastExportTime(exportRecord.lastAttemptTimestamp); + setPendingFiles(await exportService.pendingFiles(exportRecord)); + } catch (e) { + // @ts-ignore + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + log.error("syncExportRecord failed", e); + } + } + }, []); + + useEffect(() => { + if (!isDesktop) return; + + exportService.setUIUpdaters({ + setExportStage, + setExportProgress, + setLastExportTime, + setPendingFiles, + }); + const exportSettings: ExportSettings = + exportService.getExportSettings(); + setExportFolder(exportSettings?.folder ?? null); + setContinuousExport(exportSettings?.continuousExport ?? false); + void syncExportRecord(exportSettings?.folder); + }, [syncExportRecord]); + + useEffect(() => { + if (!open) return; + void syncExportRecord(exportFolder); + }, [open, exportFolder, syncExportRecord]); + + const verifyExportFolderExists = useCallback(async () => { + if (!(await exportService.exportFolderExists(exportFolder))) { + showMiniDialog({ + title: t("export_directory_does_not_exist"), + message: ( + + ), + cancel: t("ok"), + }); + return false; + } + return true; + }, [exportFolder, showMiniDialog]); + + const handleChangeExportDirectory = useCallback(() => { + void (async () => { + const newFolder = await selectAndPrepareExportDirectory(); + if (!newFolder) return; + + log.info(`Export folder changed to ${newFolder}`); + exportService.updateExportSettings({ folder: newFolder }); + setExportFolder(newFolder); + await syncExportRecord(newFolder); + })(); + }, [syncExportRecord]); + + const handleToggleContinuousExport = useCallback(() => { + void (async () => { + if (!(await verifyExportFolderExists())) return; + + const newContinuousExport = !continuousExport; + if (newContinuousExport) { + exportService.enableContinuousExport(); + } else { + exportService.disableContinuousExport(); + } + exportService.updateExportSettings({ + continuousExport: newContinuousExport, + }); + setContinuousExport(newContinuousExport); + })(); + }, [verifyExportFolderExists, continuousExport]); + + const handleStartExport = useCallback( + (opts?: ExportOpts) => { + void (async () => { + if (!(await verifyExportFolderExists())) return; + + await exportService.scheduleExport(opts ?? {}); + })(); + }, + [verifyExportFolderExists], + ); + + const handleResyncExport = useCallback(() => { + handleStartExport({ resync: true }); + }, [handleStartExport]); + + const handleStopExport = useCallback(() => { + void exportService.stopRunningExport(); + }, []); + + return ( + + + {t("export_data")} + + + + + + + + + + + + + ); +}; + +interface ExportDirectoryProps { + exportStage: ExportStage; + exportFolder: string; + onChangeExportDirectory: () => void; +} + +const ExportDirectory: React.FC = ({ + exportStage, + exportFolder, + onChangeExportDirectory, +}) => ( + + + {t("destination")} + + {exportFolder ? ( + <> + + {exportStage === ExportStage.finished || + exportStage === ExportStage.init ? ( + + ) : ( + // Prevent layout shift. + + )} + + ) : ( + + )} + +); + +interface DirectoryPathProps { + path: string; +} + +const DirectoryPath: React.FC = ({ path }) => ( + void ensureElectron().openDirectory(path)}> + + + {path} + + + +); + +const ChangeDirectoryOption: React.FC = ({ onClick }) => ( + + }> + {t("change_folder")} + + +); + +interface ContinuousExportProps { + /** + * If `true`, then continuous export is shown as enabled. + */ + enabled: boolean; + /** + * Called when the user wants to toggle the current value of + * {@link enabled}. + */ + onToggle: () => void; +} + +const ContinuousExport: React.FC = ({ + enabled, + onToggle, +}) => ( + + + {t("sync_continuously")} + + + + + +); + +type ExportDialogStageContentProps = ExportInitDialogContentProps & + ExportInProgressDialogContentProps & + ExportFinishedDialogContentProps; + +const ExportDialogStageContent: React.FC = ({ + exportStage, + exportProgress, + pendingFiles, + lastExportTime, + collectionNameByID, + onClose, + onStartExport, + onStopExport, + onResyncExport, +}) => { + switch (exportStage) { + case ExportStage.init: + return ; + + case ExportStage.migration: + case ExportStage.starting: + case ExportStage.exportingFiles: + case ExportStage.renamingCollectionFolders: + case ExportStage.trashingDeletedFiles: + case ExportStage.trashingDeletedCollections: + return ( + + ); + case ExportStage.finished: + return ( + + ); + + default: + return <>; + } +}; + +interface ExportInitDialogContentProps { + onStartExport: () => void; +} + +const ExportInitDialogContent: React.FC = ({ + onStartExport, +}) => ( + + + + {t("start")} + + + +); + +interface ExportInProgressDialogContentProps { + exportStage: ExportStage; + exportProgress: ExportProgress; + onClose: () => void; + onStopExport: () => void; +} + +const ExportInProgressDialogContent: React.FC< + ExportInProgressDialogContentProps +> = ({ exportStage, exportProgress, onClose, onStopExport }) => ( + <> + + + + {exportStage === ExportStage.starting ? ( + t("export_starting") + ) : exportStage === ExportStage.migration ? ( + t("export_preparing") + ) : exportStage === + ExportStage.renamingCollectionFolders ? ( + t("export_renaming_album_folders") + ) : exportStage === ExportStage.trashingDeletedFiles ? ( + t("export_trashing_deleted_files") + ) : exportStage === + ExportStage.trashingDeletedCollections ? ( + t("export_trashing_deleted_albums") + ) : ( + + + ), + }} + values={{ progress: exportProgress }} + /> + + )} + + + + {exportStage === ExportStage.exportingFiles ? ( + + ) : ( + + )} + + + + + + {t("close")} + + + {t("stop")} + + + +); + +interface ExportFinishedDialogContentProps { + pendingFiles: EnteFile[]; + lastExportTime: number; + collectionNameByID: Map; + onClose: () => void; + onResyncExport: () => void; +} + +const ExportFinishedDialogContent: React.FC< + ExportFinishedDialogContentProps +> = ({ + pendingFiles, + lastExportTime, + collectionNameByID, + onClose, + onResyncExport, +}) => { + const { show: showPendingList, props: pendingListVisibilityProps } = + useModalVisibility(); + + return ( + <> + + + + + {t("pending_items")} + + {pendingFiles.length ? ( + + {formattedNumber(pendingFiles.length)} + + ) : ( + + {formattedNumber(pendingFiles.length)} + + )} + + + + {t("last_export_time")} + + + {lastExportTime + ? formattedDateTime(new Date(lastExportTime)) + : t("never")} + + + + + + + {t("close")} + + + {t("export_again")} + + + + + ); +}; + +type ExportPendingListDialogProps = ModalVisibilityProps & + ExportPendingListItemData; + +const ExportPendingListDialog: React.FC = ({ + open, + onClose, + collectionNameByID, + pendingFiles, +}) => { + const itemSize = 56; /* px */ + const itemCount = pendingFiles.length; + const listHeight = Math.min(itemCount * itemSize, 240); + + const itemKey: ListItemKeySelector = ( + index, + { pendingFiles }, + ) => { + const file = pendingFiles[index]!; + return `${file.collectionID}/${file.id}`; + }; + + return ( + + + {ExportPendingListItem} + + + {t("close")} + + + ); +}; + +interface ExportPendingListItemData { + pendingFiles: EnteFile[]; + collectionNameByID: Map; +} + +const ExportPendingListItem: React.FC< + ListChildComponentProps +> = memo(({ index, style, data }) => { + const { pendingFiles, collectionNameByID } = data; + const file = pendingFiles[index]!; + + const fileName = file.metadata.title; + const collectionName = collectionNameByID.get(file.collectionID); + + return ( +
+ + + + + + +
+ ); +}, areEqual); diff --git a/web/packages/new/photos/components/ImageEditorOverlay.tsx b/web/packages/new/photos/components/ImageEditorOverlay.tsx index 9e82a91b06..aac0019815 100644 --- a/web/packages/new/photos/components/ImageEditorOverlay.tsx +++ b/web/packages/new/photos/components/ImageEditorOverlay.tsx @@ -49,7 +49,6 @@ import { downloadManager } from "ente-gallery/services/download"; import type { Collection } from "ente-media/collection"; import type { EnteFile } from "ente-media/file"; import { getLocalCollections } from "ente-new/photos/services/collections"; -import { CenteredFlex } from "ente-shared/components/Container"; import { t } from "i18next"; import React, { forwardRef, @@ -610,7 +609,7 @@ export const ImageEditorOverlay: React.FC = ({ )} {currentTab == "crop" && ( - + - + )} diff --git a/web/packages/new/photos/components/Notification.tsx b/web/packages/new/photos/components/Notification.tsx index 6e304f5d68..5b59331474 100644 --- a/web/packages/new/photos/components/Notification.tsx +++ b/web/packages/new/photos/components/Notification.tsx @@ -223,6 +223,9 @@ export const Notification: React.FC = ({ // Buttons cannot be nested in buttons, so we use a div // here instead. component="div" + // Inherit the color of the parent button instead of + // using the IconButton defaults. + color="inherit" onClick={handleClose} sx={{ bgcolor: "fill.faint" }} > diff --git a/web/packages/new/photos/components/SearchBar.tsx b/web/packages/new/photos/components/SearchBar.tsx index 0240bbdf4c..f242aee020 100644 --- a/web/packages/new/photos/components/SearchBar.tsx +++ b/web/packages/new/photos/components/SearchBar.tsx @@ -13,10 +13,13 @@ import { useTheme, type Theme, } from "@mui/material"; -import { assertionFailed } from "ente-base/assert"; import { EnteLogo, EnteLogoBox } from "ente-base/components/EnteLogo"; import type { ButtonishProps } from "ente-base/components/mui"; import { useIsSmallWidth } from "ente-base/components/utils/hooks"; +import { + hlsGenerationStatusSnapshot, + isHLSGenerationSupported, +} from "ente-gallery/services/video"; import { ItemCard, PreviewItemTile } from "ente-new/photos/components/Tiles"; import { isMLSupported, mlStatusSnapshot } from "ente-new/photos/services/ml"; import { searchOptionsForString } from "ente-new/photos/services/search"; @@ -38,6 +41,7 @@ import AsyncSelect from "react-select/async"; import { SearchPeopleList } from "./PeopleList"; import { UnstyledButton } from "./UnstyledButton"; import { + useHLSGenerationStatusSnapshot, useMLStatusSnapshot, usePeopleStateSnapshot, } from "./utils/use-snapshot"; @@ -382,11 +386,26 @@ const shouldShowEmptyState = (inputValue: string) => { // Don't show empty state if the user has entered search input. if (inputValue) return false; - // Don't show empty state if there is no ML related information. - if (!isMLSupported) return false; + // Don't show empty state if there is no ML related information AND we're + // not processing videos. - const status = mlStatusSnapshot(); - if (!status || status.phase == "disabled") return false; + if (!isMLSupported && !isHLSGenerationSupported) { + // Neither of ML or HLS generation is supported on current client. This + // is the code path for web. + return false; + } + + const mlStatus = mlStatusSnapshot(); + const vpStatus = hlsGenerationStatusSnapshot(); + if ( + (!mlStatus || mlStatus.phase == "disabled") && + (!vpStatus?.enabled || vpStatus.status != "processing") + ) { + // ML is either not supported or currently disabled AND video processing + // is either not supported or currently not happening. Don't show the + // empty state. + return false; + } // Show it otherwise. return true; @@ -401,15 +420,18 @@ const EmptyState: React.FC< > = ({ onSelectPeople, onSelectPerson }) => { const mlStatus = useMLStatusSnapshot(); const people = usePeopleStateSnapshot()?.visiblePeople; - - if (!mlStatus || mlStatus.phase == "disabled") { - // The preflight check should've prevented us from coming here. - assertionFailed(); - return <>; - } + const vpStatus = useHLSGenerationStatusSnapshot(); let label: string | undefined; - switch (mlStatus.phase) { + switch (mlStatus?.phase) { + case undefined: + case "disabled": + case "done": + // If ML is not running, see if video processing is. + if (vpStatus?.enabled && vpStatus.status == "processing") { + label = t("processing_videos_status"); + } + break; case "scheduled": label = t("indexing_scheduled"); break; @@ -424,6 +446,12 @@ const EmptyState: React.FC< break; } + // If ML is disabled and we're not video processing, then don't show the + // empty state content. + if ((!mlStatus || mlStatus.phase == "disabled") && !label) { + return <>; + } + return ( {people && people.length > 0 && ( @@ -473,7 +501,11 @@ const OptionContents = ({ data: option }: { data: SearchOption }) => ( > {option.suggestion.label} diff --git a/web/packages/new/photos/components/Tiles.tsx b/web/packages/new/photos/components/Tiles.tsx index 4c95b2b216..2572257453 100644 --- a/web/packages/new/photos/components/Tiles.tsx +++ b/web/packages/new/photos/components/Tiles.tsx @@ -2,6 +2,7 @@ import AddIcon from "@mui/icons-material/Add"; import { Stack, styled, Typography } from "@mui/material"; import { CenteredFill, Overlay } from "ente-base/components/containers"; import type { ButtonishProps } from "ente-base/components/mui"; +import log from "ente-base/log"; import { downloadManager } from "ente-gallery/services/download"; import { type EnteFile } from "ente-media/file"; import { @@ -76,7 +77,10 @@ export const ItemCard: React.FC> = ({ } else { void downloadManager .renderableThumbnailURL(coverFile, isScrolling) - .then((url) => !didCancel && setCoverImageURL(url)); + .then((url) => !didCancel && setCoverImageURL(url)) + .catch((e: unknown) => { + log.warn("Failed to fetch thumbnail", e); + }); } return () => { diff --git a/web/packages/new/photos/components/gallery/helpers.ts b/web/packages/new/photos/components/gallery/helpers.ts index 39785fd64a..afce9688d9 100644 --- a/web/packages/new/photos/components/gallery/helpers.ts +++ b/web/packages/new/photos/components/gallery/helpers.ts @@ -10,10 +10,32 @@ * is a needed for fast refresh to work. */ +import log from "ente-base/log"; import type { Collection } from "ente-media/collection"; import type { FamilyData } from "ente-new/photos/services/user-details"; +import { getRecoveryKey } from "ente-shared/crypto/helpers"; import type { User } from "ente-shared/user/types"; +/** + * Ensure that the keys in local storage are not malformed by verifying that the + * recoveryKey can be decrypted with the masterKey. + * + * This is not meant to be bullet proof, but more like an extra sanity check. + * + * @returns `true` if the sanity check passed, otherwise `false`. Since failure + * is not expected, the caller should {@link logout} on `false` to avoid + * continuing with an unexpected local state. + */ +export const validateKey = async () => { + try { + await getRecoveryKey(); + return true; + } catch (e) { + log.warn("Failed to validate key" /*, caller will logout */, e); + return false; + } +}; + export const constructUserIDToEmailMap = ( user: User, collections: Collection[], @@ -30,7 +52,7 @@ export const constructUserIDToEmailMap = ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (sharees) { sharees.forEach((item) => { - if (item.id !== user.id) + if (item.id !== user.id && item.email) userIDToEmailMap.set(item.id, item.email); }); } @@ -56,10 +78,11 @@ export const createShareeSuggestionEmails = ( // type for Collection. // // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - return (sharees ?? []).map((sharee) => sharee.email); + return (sharees ?? []).map(({ email }) => email); } }) - .flat(); + .flat() + .filter((e) => e !== undefined); // Add family members. if (familyData) { diff --git a/web/packages/new/photos/components/gallery/index.tsx b/web/packages/new/photos/components/gallery/index.tsx index 8cd9b0cb68..66edbedb6b 100644 --- a/web/packages/new/photos/components/gallery/index.tsx +++ b/web/packages/new/photos/components/gallery/index.tsx @@ -7,11 +7,17 @@ * there. */ -import { Paper, Stack, Typography } from "@mui/material"; +import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternateOutlined"; +import FolderIcon from "@mui/icons-material/FolderOutlined"; +import { Paper, Stack, styled, Typography } from "@mui/material"; import { CenteredFill } from "ente-base/components/containers"; +import { EnteLogo } from "ente-base/components/EnteLogo"; +import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; +import { type UploadTypeSelectorIntent } from "ente-gallery/components/Upload"; import type { SearchSuggestion } from "ente-new/photos/services/search/types"; import { t } from "i18next"; import React, { useState } from "react"; +import { Trans } from "react-i18next"; import { enableML } from "../../services/ml"; import { EnableML, FaceConsent } from "../sidebar/MLSettings"; import { useMLStatusSnapshot } from "../utils/use-snapshot"; @@ -51,6 +57,94 @@ export const SearchResultsHeader: React.FC = ({ ); +interface GalleryEmptyStateProps { + /** + * If `true`, then an upload is already in progress (the empty state will + * then disable the prompts for uploads). + */ + isUploadInProgress: boolean; + /** + * Called when the user selects one of the upload buttons. It is passed the + * "intent" of the user. + */ + onUpload: (intent: UploadTypeSelectorIntent) => void; +} + +export const GalleryEmptyState: React.FC = ({ + isUploadInProgress, + onUpload, +}) => ( + + + + }} + /> + + + {t("welcome_to_ente_subtitle")} + + + + + onUpload("upload")} + disabled={isUploadInProgress} + sx={{ p: 1 }} + > + + + {t("upload_first_photo")} + + + onUpload("import")} + disabled={isUploadInProgress} + sx={{ p: 1 }} + > + + + {t("import_your_folders")} + + + + +); + +/** + * Prevent the image from being selected _and_ dragged, since dragging it + * triggers the our dropdown selector overlay. + */ +const NonDraggableImage = styled("img")` + pointer-events: none; + user-select: none; +`; + export const PeopleEmptyState: React.FC = () => { const mlStatus = useMLStatusSnapshot(); diff --git a/web/packages/new/photos/components/gallery/reducer.ts b/web/packages/new/photos/components/gallery/reducer.ts index d650749082..c3464c7bf7 100644 --- a/web/packages/new/photos/components/gallery/reducer.ts +++ b/web/packages/new/photos/components/gallery/reducer.ts @@ -543,6 +543,7 @@ const galleryReducer: React.Reducer = ( hiddenFileIDs, archivedFileIDs, favoriteFileIDs: deriveFavoriteFileIDs( + action.user, normalCollections, normalFiles, state.unsyncedFavoriteUpdates, @@ -617,6 +618,7 @@ const galleryReducer: React.Reducer = ( deriveDefaultHiddenCollectionIDs(hiddenCollections), archivedFileIDs, favoriteFileIDs: deriveFavoriteFileIDs( + state.user!, normalCollections, state.normalFiles, state.unsyncedFavoriteUpdates, @@ -669,6 +671,7 @@ const galleryReducer: React.Reducer = ( archivedCollectionIDs, archivedFileIDs, favoriteFileIDs: deriveFavoriteFileIDs( + state.user!, normalCollections, state.normalFiles, state.unsyncedFavoriteUpdates, @@ -892,6 +895,7 @@ const galleryReducer: React.Reducer = ( return { ...state, favoriteFileIDs: deriveFavoriteFileIDs( + state.user!, state.normalCollections, state.normalFiles, unsyncedFavoriteUpdates, @@ -941,6 +945,7 @@ const galleryReducer: React.Reducer = ( const unsyncedFavoriteUpdates: GalleryState["unsyncedFavoriteUpdates"] = new Map(); const favoriteFileIDs = deriveFavoriteFileIDs( + state.user!, state.normalCollections, state.normalFiles, unsyncedFavoriteUpdates, @@ -1211,13 +1216,15 @@ const deriveArchivedFileIDs = ( * Compute favorite file IDs from their dependencies. */ const deriveFavoriteFileIDs = ( + user: User, collections: Collection[], files: EnteFile[], unsyncedFavoriteUpdates: GalleryState["unsyncedFavoriteUpdates"], ) => { let favoriteFileIDs = new Set(); for (const collection of collections) { - if (collection.type == "favorites") { + // See: [Note: User and shared favorites] + if (collection.type == "favorites" && collection.owner.id == user.id) { favoriteFileIDs = new Set( files .filter((file) => file.collectionID === collection.id) @@ -1350,6 +1357,26 @@ const createCollectionSummaries = ( } else { type = "incomingShareViewer"; } + } else if (collection.type == "favorites") { + // [Note: User and shared favorites] + // + // "favorites" can be both the user's own favorites, or favorites of + // other users shared with them. However, all of the latter will get + // typed as "incomingShareViewer" or "incomingShareCollaborator" in + // the first case above. So if a collection summary has type + // "favorites", it is guaranteed to be the user's own favorites. + // + // However, notice that the type of the _collection_ itself is not + // changed, so whenever we're checking the type of the collection + // (not of the collection summary) and we specifically want to + // target the user's own favorites, we also need to check the + // collection owner's ID is the same as the logged in user's ID. + // + // This case needs to be above the other cases since the primary + // classification of this collection summary is that it is the + // user's "favorites", everything else is secondary and can be part + // of the `attributes` computed below. + type = collection.type; } else if (isOutgoingShare(collection, user)) { type = "outgoingShare"; } else if (isSharedOnlyViaLink(collection)) { @@ -1389,15 +1416,38 @@ const createCollectionSummaries = ( if (isPinnedCollection(collection)) { attributes.push("pinned"); } - // TODO: Verify type before removing the null check. - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (collection.type) attributes.push(collection.type); + switch (collection.type) { + case "favorites": + // We don't want to treat other folks' favorites specially like + // the user's own favorites (giving it a special icon etc). + if (collection.owner.id == user.id) + attributes.push(collection.type); + break; + default: + // TODO: Verify type before removing the null check. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (collection.type) attributes.push(collection.type); + break; + } let name: string; if (type == "uncategorized") { name = t("section_uncategorized"); } else if (type == "favorites") { name = t("favorites"); + } else if (collection.type == "favorites") { + // See: [Note: User and shared favorites] above. + // + // Use the first letter of the email of the user who shared this + // particular favorite as a prefix to disambiguate this collection + // from the user's own favorites. + // TODO(FAV): Use the person name when avail + const initial = collection.owner.email?.at(0)?.toUpperCase(); + if (initial) { + name = t("person_favorites", { name: initial }); + } else { + name = t("shared_favorites"); + } } else { name = collection.name; } @@ -1640,6 +1690,7 @@ const stateForUpdatedNormalFiles = ( normalFiles, ), favoriteFileIDs: deriveFavoriteFileIDs( + state.user!, state.normalCollections, normalFiles, state.unsyncedFavoriteUpdates, diff --git a/web/packages/new/photos/components/sidebar/MLSettings.tsx b/web/packages/new/photos/components/sidebar/MLSettings.tsx index 98b3319d96..9951fafad3 100644 --- a/web/packages/new/photos/components/sidebar/MLSettings.tsx +++ b/web/packages/new/photos/components/sidebar/MLSettings.tsx @@ -13,8 +13,7 @@ import { RowButtonGroup, RowSwitch } from "ente-base/components/RowButton"; import { ActivityIndicator } from "ente-base/components/mui/ActivityIndicator"; import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; import { - NestedSidebarDrawer, - SidebarDrawerTitlebar, + TitledNestedSidebarDrawer, type NestedSidebarDrawerVisibilityProps, } from "ente-base/components/mui/SidebarDrawer"; import { useBaseContext } from "ente-base/context"; @@ -66,19 +65,13 @@ export const MLSettings: React.FC = ({ return ( <> - - - - {component} - - + {component} + = ({ }; return ( - - - - - - + + ); }; diff --git a/web/packages/new/photos/components/sidebar/TwoFactorSettings.tsx b/web/packages/new/photos/components/sidebar/TwoFactorSettings.tsx index 5338951cae..3e88743ece 100644 --- a/web/packages/new/photos/components/sidebar/TwoFactorSettings.tsx +++ b/web/packages/new/photos/components/sidebar/TwoFactorSettings.tsx @@ -8,8 +8,7 @@ import { } from "ente-base/components/RowButton"; import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; import { - NestedSidebarDrawer, - SidebarDrawerTitlebar, + TitledNestedSidebarDrawer, type NestedSidebarDrawerVisibilityProps, } from "ente-base/components/mui/SidebarDrawer"; import { useBaseContext } from "ente-base/context"; @@ -52,24 +51,17 @@ export const TwoFactorSettings: React.FC< }; return ( - - - - - {isTwoFactorEnabled ? ( - - ) : ( - - )} - - + {isTwoFactorEnabled ? ( + + ) : ( + + )} + ); }; diff --git a/web/packages/new/photos/components/utils/use-snapshot.ts b/web/packages/new/photos/components/utils/use-snapshot.ts index 3cf14122aa..21073d6155 100644 --- a/web/packages/new/photos/components/utils/use-snapshot.ts +++ b/web/packages/new/photos/components/utils/use-snapshot.ts @@ -1,3 +1,7 @@ +import { + hlsGenerationStatusSnapshot, + hlsGenerationStatusSubscribe, +} from "ente-gallery/services/video"; import { useSyncExternalStore } from "react"; import { mlStatusSnapshot, @@ -38,3 +42,13 @@ export const useMLStatusSnapshot = () => */ export const usePeopleStateSnapshot = () => useSyncExternalStore(peopleStateSubscribe, peopleStateSnapshot); + +/** + * A convenience hook that returns {@link hlsGenerationStatusSnapshot}, and also + * subscribes to updates. + */ +export const useHLSGenerationStatusSnapshot = () => + useSyncExternalStore( + hlsGenerationStatusSubscribe, + hlsGenerationStatusSnapshot, + ); diff --git a/web/packages/new/photos/services/collection.ts b/web/packages/new/photos/services/collection.ts index cac52c9771..25aca079b0 100644 --- a/web/packages/new/photos/services/collection.ts +++ b/web/packages/new/photos/services/collection.ts @@ -50,6 +50,9 @@ export const findDefaultHiddenCollectionIDs = (collections: Collection[]) => .map((collection) => collection.id), ); +/** + * Return true if this is a collection that the user doesn't own. + */ export const isIncomingShare = (collection: Collection, user: User) => collection.owner.id !== user.id; diff --git a/web/packages/new/photos/services/collections.ts b/web/packages/new/photos/services/collections.ts index 8e1833f28e..2ee70780a0 100644 --- a/web/packages/new/photos/services/collections.ts +++ b/web/packages/new/photos/services/collections.ts @@ -343,9 +343,29 @@ export async function cleanTrashCollections(fileTrash: Trash) { async function getLastTrashSyncTime() { return (await localForage.getItem(TRASH_TIME)) ?? 0; } + +/** + * Update our locally saved data about the files and collections in trash by + * syncing with remote. + * + * The sync uses a diff-based mechanism that syncs forward from the last sync + * time (also persisted). + * + * @param onUpdateTrashFiles A callback invoked when the locally persisted trash + * items are updated. This can be used for the UI to also update its state. This + * callback can be invoked multiple times during the sync (once for each batch + * that gets processed). + * + * @param onPruneDeletedFileIDs A callback invoked when files that were + * previously in trash have now been permanently deleted. This can be used by + * other subsystems to prune data referring to files that now have been deleted + * permanently. This callback can be invoked multiple times during the sync + * (once for each batch that gets processed). + */ export async function syncTrash( collections: Collection[], - setTrashedFiles: ((fs: EnteFile[]) => void) | undefined, + onUpdateTrashFiles: ((files: EnteFile[]) => void) | undefined, + onPruneDeletedFileIDs: (deletedFileIDs: Set) => Promise, ): Promise { const trash = await getLocalTrash(); collections = [...collections, ...(await getLocalDeletedCollections())]; @@ -359,21 +379,23 @@ export async function syncTrash( const updatedTrash = await updateTrash( collectionMap, - lastSyncTime, - setTrashedFiles, trash, + lastSyncTime, + onUpdateTrashFiles, + onPruneDeletedFileIDs, ); await cleanTrashCollections(updatedTrash); } -export const updateTrash = async ( +const updateTrash = async ( collections: Map, - sinceTime: number, - setTrashedFiles: ((fs: EnteFile[]) => void) | undefined, currentTrash: Trash, + sinceTime: number, + onUpdateTrashFiles: ((files: EnteFile[]) => void) | undefined, + onPruneDeletedFileIDs: (deletedFileIDs: Set) => Promise, ): Promise => { + let updatedTrash: Trash = [...currentTrash]; try { - let updatedTrash: Trash = [...currentTrash]; let time = sinceTime; let resp; @@ -387,6 +409,7 @@ export const updateTrash = async ( { sinceTime: time }, { "X-Auth-Token": token }, ); + const deletedFileIDs = new Set(); // #Perf: This can be optimized by running the decryption in parallel for (const trashItem of resp.data.diff as EncryptedTrashItem[]) { const collectionID = trashItem.file.collectionID; @@ -398,6 +421,9 @@ export const updateTrash = async ( ...collections.values(), ]); } + if (trashItem.isDeleted) { + deletedFileIDs.add(trashItem.file.id); + } if (!trashItem.isDeleted && !trashItem.isRestored) { const decryptedFile = await decryptFile( trashItem.file, @@ -415,15 +441,17 @@ export const updateTrash = async ( time = resp.data.diff.slice(-1)[0].updatedAt; } - setTrashedFiles?.(getTrashedFiles(updatedTrash)); + onUpdateTrashFiles?.(getTrashedFiles(updatedTrash)); + if (deletedFileIDs.size > 0) { + await onPruneDeletedFileIDs(deletedFileIDs); + } await localForage.setItem(TRASH, updatedTrash); await localForage.setItem(TRASH_TIME, time); } while (resp.data.hasMore); - return updatedTrash; } catch (e) { log.error("Get trash files failed", e); } - return currentTrash; + return updatedTrash; }; export const emptyTrash = async () => { diff --git a/web/apps/photos/src/services/export/migration.ts b/web/packages/new/photos/services/export-migration.ts similarity index 94% rename from web/apps/photos/src/services/export/migration.ts rename to web/packages/new/photos/services/export-migration.ts index bb9167a2be..2dc6823a64 100644 --- a/web/apps/photos/src/services/export/migration.ts +++ b/web/packages/new/photos/services/export-migration.ts @@ -1,13 +1,18 @@ +// TODO: Audit this file +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ import { ensureElectron } from "ente-base/electron"; import { joinPath, nameAndExtension } from "ente-base/file-name"; import log from "ente-base/log"; +import { exportMetadataDirectoryName } from "ente-gallery/export-dirs"; import { downloadManager } from "ente-gallery/services/download"; import type { Collection } from "ente-media/collection"; import { mergeMetadata, type EnteFile } from "ente-media/file"; import { FileType } from "ente-media/file-type"; import { decodeLivePhoto } from "ente-media/live-photo"; import { getLocalCollections } from "ente-new/photos/services/collections"; -import { exportMetadataDirectoryName } from "ente-new/photos/services/export"; import { getAllLocalFiles } from "ente-new/photos/services/files"; import { safeDirectoryName, @@ -17,8 +22,7 @@ import { import { getData } from "ente-shared/storage/localStorage"; import type { User } from "ente-shared/user/types"; import { wait } from "ente-utils/promise"; -import { getIDBasedSortedFiles, getPersonalFiles } from "utils/file"; -import { +import exportService, { getCollectionIDFromFileUID, getExportRecordFileUID, getLivePhotoExportName, @@ -27,8 +31,7 @@ import { type ExportProgress, type ExportStage, type FileExportNames, -} from "."; -import exportService from "./index"; +} from "./export"; type ExportedCollectionPaths = Record; @@ -139,7 +142,7 @@ async function migrationV0ToV1( const localFiles = mergeMetadata(await getAllLocalFiles()); const localCollections = await getLocalCollections(); const personalFiles = getIDBasedSortedFiles( - getPersonalFiles(localFiles, user), + localFiles.filter((file) => file.ownerID == user.id), ); const personalCollections = localCollections.filter( (collection) => collection.owner.id === user?.id, @@ -155,6 +158,10 @@ async function migrationV0ToV1( ); } +const getIDBasedSortedFiles = (files: EnteFile[]) => { + return files.sort((a, b) => a.id - b.id); +}; + async function migrationV1ToV2( exportRecord: ExportRecordV1, exportDir: string, @@ -173,7 +180,7 @@ async function migrationV2ToV3( const user: User = getData("user"); const localFiles = mergeMetadata(await getAllLocalFiles()); const personalFiles = getIDBasedSortedFiles( - getPersonalFiles(localFiles, user), + localFiles.filter((file) => file.ownerID == user.id), ); const collectionExportNames = @@ -185,7 +192,9 @@ async function migrationV2ToV3( updateProgress, ); + // @ts-ignore exportRecord.exportedCollectionPaths = undefined; + // @ts-ignore exportRecord.exportedFiles = undefined; const updatedExportRecord: ExportRecord = { ...exportRecord, @@ -253,11 +262,13 @@ async function migrateFiles( for (const file of files) { const collectionPath = collectionIDPathMap.get(file.collectionID); const metadataPath = joinPath( + // @ts-ignore collectionPath, exportMetadataDirectoryName, ); const oldFileName = `${file.id}_${oldSanitizeName(file.metadata.title)}`; + // @ts-ignore const oldFilePath = joinPath(collectionPath, oldFileName); const oldFileMetadataPath = joinPath( metadataPath, @@ -265,10 +276,12 @@ async function migrateFiles( ); const newFileName = await safeFileName( + // @ts-ignore collectionPath, file.metadata.title, fs.exists, ); + // @ts-ignore const newFilePath = joinPath(collectionPath, newFileName); const newFileMetadataPath = joinPath( metadataPath, @@ -287,12 +300,15 @@ async function removeDeprecatedExportRecordProperties( exportDir: string, ) { if (exportRecord?.queuedFiles) { + // @ts-ignore exportRecord.queuedFiles = undefined; } if (exportRecord?.progress) { + // @ts-ignore exportRecord.progress = undefined; } if (exportRecord?.failedFiles) { + // @ts-ignore exportRecord.failedFiles = undefined; } await exportService.updateExportRecord(exportDir, exportRecord); @@ -302,6 +318,7 @@ async function getCollectionExportNamesFromExportedCollectionPaths( exportRecord: ExportRecordV2, ): Promise { if (!exportRecord.exportedCollectionPaths) { + // @ts-ignore return; } const exportedCollectionNames = Object.fromEntries( @@ -314,6 +331,7 @@ async function getCollectionExportNamesFromExportedCollectionPaths( }, ), ); + // @ts-ignore return exportedCollectionNames; } @@ -330,6 +348,7 @@ async function getFileExportNamesFromExportedFiles( updateProgress: (progress: ExportProgress) => void, ): Promise { if (!exportedFiles.length) { + // @ts-ignore return; } log.info( @@ -359,11 +378,13 @@ async function getFileExportNamesFromExportedFiles( fileBlob, ); const imageExportName = getUniqueFileExportNameForMigration( + // @ts-ignore collectionPath, imageFileName, usedFilePaths, ); const videoExportName = getUniqueFileExportNameForMigration( + // @ts-ignore collectionPath, videoFileName, usedFilePaths, @@ -374,6 +395,7 @@ async function getFileExportNamesFromExportedFiles( ); } else { fileExportName = getUniqueFileExportNameForMigration( + // @ts-ignore collectionPath, file.metadata.title, usedFilePaths, @@ -384,6 +406,7 @@ async function getFileExportNamesFromExportedFiles( `file export name for ${file.metadata.title} is ${fileExportName}`, ); exportedFileNames = { + // @ts-ignore ...exportedFileNames, [getExportRecordFileUID(file)]: fileExportName, }; @@ -393,6 +416,7 @@ async function getFileExportNamesFromExportedFiles( failed: 0, }); } + // @ts-ignore return exportedFileNames; } @@ -409,6 +433,7 @@ function reMigrateCollectionExportNames( }, ), ); + // @ts-ignore return exportedCollectionNames; } @@ -548,6 +573,7 @@ const getUniqueFileExportNameForMigration = ( if (!usedFilePaths.has(collectionPath)) { usedFilePaths.set(collectionPath, new Set()); } + // @ts-ignore usedFilePaths .get(collectionPath) .add(getFileSavePath(collectionPath, fileExportName)); diff --git a/web/packages/new/photos/services/export.ts b/web/packages/new/photos/services/export.ts index 40229741ed..2c5ac352bb 100644 --- a/web/packages/new/photos/services/export.ts +++ b/web/packages/new/photos/services/export.ts @@ -1,11 +1,1582 @@ -/** - * Name of the directory in which we put our metadata when exporting to the file - * system. - */ -export const exportMetadataDirectoryName = "metadata"; +// TODO: Audit this file +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { ensureElectron } from "ente-base/electron"; +import { joinPath } from "ente-base/file-name"; +import log from "ente-base/log"; +import { + exportMetadataDirectoryName, + exportTrashDirectoryName, +} from "ente-gallery/export-dirs"; +import { downloadManager } from "ente-gallery/services/download"; +import { writeStream } from "ente-gallery/utils/native-stream"; +import type { Collection } from "ente-media/collection"; +import { mergeMetadata, type EnteFile } from "ente-media/file"; +import { fileLocation, type Metadata } from "ente-media/file-metadata"; +import { FileType } from "ente-media/file-type"; +import { decodeLivePhoto } from "ente-media/live-photo"; +import { + createCollectionNameByID, + getCollectionUserFacingName, +} from "ente-new/photos/services/collection"; +import { getAllLocalCollections } from "ente-new/photos/services/collections"; +import { getAllLocalFiles } from "ente-new/photos/services/files"; +import { + safeDirectoryName, + safeFileName, +} from "ente-new/photos/utils/native-fs"; +import { CustomError } from "ente-shared/error"; +import { getData, setData } from "ente-shared/storage/localStorage"; +import { PromiseQueue } from "ente-utils/promise"; +import i18n from "i18next"; +import { migrateExport, type ExportRecord } from "./export-migration"; + +/** Name of the JSON file in which we keep the state of the export. */ +const exportRecordFileName = "export_status.json"; /** - * Name of the directory in which we keep trash items when deleting files that - * have been exported to the local disk previously. + * Name of the top level directory which we create underneath the selected + * directory when the user starts an export to the file system. */ -export const exportTrashDirectoryName = "Trash"; +const exportDirectoryName = "Ente Photos"; + +export const ExportStage = { + init: 0, + migration: 1, + starting: 2, + exportingFiles: 3, + trashingDeletedFiles: 4, + renamingCollectionFolders: 5, + trashingDeletedCollections: 6, + finished: 7, +} as const; + +export type ExportStage = (typeof ExportStage)[keyof typeof ExportStage]; + +export interface ExportProgress { + success: number; + failed: number; + total: number; +} + +export interface ExportSettings { + folder: string; + continuousExport: boolean; +} + +export type CollectionExportNames = Record; + +export type FileExportNames = Record; + +export const NULL_EXPORT_RECORD: ExportRecord = { + version: 3, + // @ts-ignore + lastAttemptTimestamp: null, + stage: ExportStage.init, + fileExportNames: {}, + collectionExportNames: {}, +}; + +export interface ExportOpts { + /** + * If true, perform an additional on-disk check to determine which files + * need to be exported. + * + * This has performance implications for huge libraries, so we only do this: + * - For the first export after an app start + * - If the user explicitly presses the "Resync" button. + */ + resync?: boolean; +} + +interface ExportUIUpdaters { + setExportStage: (stage: ExportStage) => void; + setExportProgress: (progress: ExportProgress) => void; + setLastExportTime: (exportTime: number) => void; + setPendingFiles: (pendingFiles: EnteFile[]) => void; +} + +interface RequestCanceller { + exec: () => void; +} + +interface CancellationStatus { + status: boolean; +} + +class ExportService { + // @ts-ignore + private exportSettings: ExportSettings; + // @ts-ignore + private exportInProgress: RequestCanceller = null; + private resync = true; + private reRunNeeded = false; + private exportRecordUpdater = new PromiseQueue(); + // @ts-ignore + private continuousExportEventHandler: () => void; + private uiUpdater: ExportUIUpdaters = { + setExportProgress: () => {}, + setExportStage: () => {}, + setLastExportTime: () => {}, + setPendingFiles: () => {}, + }; + private currentExportProgress: ExportProgress = { + total: 0, + success: 0, + failed: 0, + }; + // @ts-ignore + private cachedMetadataDateTimeFormatter: Intl.DateTimeFormat; + + getExportSettings(): ExportSettings { + try { + if (this.exportSettings) { + return this.exportSettings; + } + const exportSettings = getData("export"); + this.exportSettings = exportSettings; + return exportSettings; + } catch (e) { + log.error("getExportSettings failed", e); + throw e; + } + } + + updateExportSettings(newData: Partial) { + try { + const exportSettings = this.getExportSettings(); + const newSettings = { ...exportSettings, ...newData }; + this.exportSettings = newSettings; + setData("export", newSettings); + } catch (e) { + log.error("updateExportSettings failed", e); + throw e; + } + } + + async runMigration( + exportDir: string, + exportRecord: ExportRecord, + updateProgress: (progress: ExportProgress) => void, + ) { + try { + log.info("running migration"); + await migrateExport(exportDir, exportRecord, updateProgress); + log.info("migration completed"); + } catch (e) { + log.error("migration failed", e); + throw e; + } + } + + setUIUpdaters(uiUpdater: ExportUIUpdaters) { + this.uiUpdater = uiUpdater; + this.uiUpdater.setExportProgress(this.currentExportProgress); + } + + private updateExportProgress(exportProgress: ExportProgress) { + this.currentExportProgress = exportProgress; + this.uiUpdater.setExportProgress(exportProgress); + } + + private async updateExportStage(stage: ExportStage) { + const exportFolder = this.getExportSettings()?.folder; + await this.updateExportRecord(exportFolder, { stage }); + this.uiUpdater.setExportStage(stage); + } + + private async updateLastExportTime(exportTime: number) { + const exportFolder = this.getExportSettings()?.folder; + await this.updateExportRecord(exportFolder, { + lastAttemptTimestamp: exportTime, + }); + this.uiUpdater.setLastExportTime(exportTime); + } + + private resyncOnce() { + const resync = this.resync; + this.resync = false; + return resync; + } + + resumeExport() { + this.scheduleExport({ resync: this.resyncOnce() }); + } + + enableContinuousExport() { + // @ts-ignore + if (this.continuousExportEventHandler) { + log.warn("Continuous export already enabled"); + return; + } + this.continuousExportEventHandler = () => { + this.scheduleExport({ resync: this.resyncOnce() }); + }; + this.continuousExportEventHandler(); + } + + disableContinuousExport() { + if (!this.continuousExportEventHandler) { + log.warn("Continuous export already disabled"); + return; + } + // @ts-ignore + this.continuousExportEventHandler = null; + } + + /** + * Called when the local database of files changes. + */ + onLocalFilesUpdated() { + if (this.continuousExportEventHandler) { + this.continuousExportEventHandler(); + } + } + + /** + * Return the list of files that have not yet been exported. + * + * @param exportRecord The export record containing information about the + * export, including files which've been exported. If an export record is + * not specified (e.g. if the user has not exported anything yet and we just + * wish to show a preview of what will be exported), then the function will + * return the list of all files that will be exported if an export were to + * happen. + */ + pendingFiles = async (exportRecord?: ExportRecord): Promise => { + return getUnExportedFiles( + await getAllLocalFiles(), + exportRecord, + undefined, + ); + }; + + async preExport(exportFolder: string) { + await this.verifyExportFolderExists(exportFolder); + const exportRecord = await this.getExportRecord(exportFolder); + await this.updateExportStage(ExportStage.migration); + await this.runMigration( + exportFolder, + exportRecord, + this.updateExportProgress.bind(this), + ); + await this.updateExportStage(ExportStage.starting); + } + + async postExport() { + try { + const exportFolder = this.getExportSettings()?.folder; + if (!(await this.exportFolderExists(exportFolder))) { + this.uiUpdater.setExportStage(ExportStage.init); + return; + } + await this.updateExportStage(ExportStage.finished); + await this.updateLastExportTime(Date.now()); + this.uiUpdater.setPendingFiles( + await this.pendingFiles( + await this.getExportRecord(exportFolder), + ), + ); + } catch (e) { + log.error("postExport failed", e); + } + } + + async stopRunningExport() { + try { + log.info("user requested export cancellation"); + this.exportInProgress.exec(); + // @ts-ignore + this.exportInProgress = null; + this.reRunNeeded = false; + await this.postExport(); + } catch (e) { + log.error("stopRunningExport failed", e); + } + } + + scheduleExport = async (exportOpts: ExportOpts) => { + try { + if (this.exportInProgress) { + log.info("export in progress, scheduling re-run"); + this.reRunNeeded = true; + return; + } else { + log.info("export not in progress, starting export"); + } + + const isCanceled: CancellationStatus = { status: false }; + const canceller: RequestCanceller = { + exec: () => { + isCanceled.status = true; + }, + }; + this.exportInProgress = canceller; + try { + const exportFolder = this.getExportSettings()?.folder; + await this.preExport(exportFolder); + log.info("export started"); + await this.runExport(exportFolder, isCanceled, exportOpts); + log.info("export completed"); + } finally { + if (isCanceled.status) { + log.info("export cancellation done"); + if (!this.exportInProgress) { + await this.postExport(); + } + } else { + await this.postExport(); + log.info("resetting export in progress after completion"); + // @ts-ignore + this.exportInProgress = null; + if (this.reRunNeeded) { + this.reRunNeeded = false; + log.info("re-running export"); + setTimeout(() => this.scheduleExport(exportOpts), 0); + } + } + } + } catch (e) { + if ( + // @ts-ignore + e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && + // @ts-ignore + e.message !== CustomError.EXPORT_STOPPED + ) { + log.error("scheduleExport failed", e); + } + } + }; + + private async runExport( + exportFolder: string, + isCanceled: CancellationStatus, + { resync }: ExportOpts, + ) { + try { + const files = mergeMetadata(await getAllLocalFiles()); + const collections = await getAllLocalCollections(); + + const exportRecord = await this.getExportRecord(exportFolder); + const collectionIDExportNameMap = + convertCollectionIDExportNameObjectToMap( + exportRecord.collectionExportNames, + ); + const collectionIDNameMap = createCollectionNameByID(collections); + + const renamedCollections = getRenamedExportedCollections( + collections, + exportRecord, + ); + + const removedFileUIDs = getDeletedExportedFiles( + files, + exportRecord, + ); + + const diskFileRecordIDs = resync + ? await readOnDiskFileExportRecordIDs( + files, + collectionIDExportNameMap, + exportFolder, + exportRecord, + isCanceled, + ) + : undefined; + + const filesToExport = getUnExportedFiles( + files, + exportRecord, + diskFileRecordIDs, + ); + + const deletedExportedCollections = getDeletedExportedCollections( + collections, + exportRecord, + ); + + log.info( + `[export] files: ${files.length}, disk files: ${diskFileRecordIDs?.size ?? ""}, unexported files: ${filesToExport.length}, deleted exported files: ${removedFileUIDs.length}, renamed collections: ${renamedCollections.length}, deleted collections: ${deletedExportedCollections.length}`, + ); + let success = 0; + let failed = 0; + this.uiUpdater.setExportProgress({ + success: success, + failed: failed, + total: filesToExport.length, + }); + const incrementSuccess = () => { + this.updateExportProgress({ + success: ++success, + failed: failed, + total: filesToExport.length, + }); + }; + const incrementFailed = () => { + this.updateExportProgress({ + success: success, + failed: ++failed, + total: filesToExport.length, + }); + }; + if (renamedCollections?.length > 0) { + this.updateExportStage(ExportStage.renamingCollectionFolders); + log.info(`renaming ${renamedCollections.length} collections`); + await this.collectionRenamer( + exportFolder, + collectionIDExportNameMap, + renamedCollections, + isCanceled, + ); + } + + if (removedFileUIDs?.length > 0) { + this.updateExportStage(ExportStage.trashingDeletedFiles); + log.info(`trashing ${removedFileUIDs.length} files`); + await this.fileTrasher( + exportFolder, + collectionIDExportNameMap, + removedFileUIDs, + isCanceled, + ); + } + if (filesToExport?.length > 0) { + this.updateExportStage(ExportStage.exportingFiles); + log.info(`exporting ${filesToExport.length} files`); + await this.fileExporter( + filesToExport, + collectionIDNameMap, + collectionIDExportNameMap, + exportFolder, + incrementSuccess, + incrementFailed, + isCanceled, + ); + } + if (deletedExportedCollections?.length > 0) { + this.updateExportStage(ExportStage.trashingDeletedCollections); + log.info( + `removing ${deletedExportedCollections.length} collections`, + ); + await this.collectionRemover( + deletedExportedCollections, + exportFolder, + isCanceled, + ); + } + } catch (e) { + if ( + // @ts-ignore + e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && + // @ts-ignore + e.message !== CustomError.EXPORT_STOPPED + ) { + log.error("runExport failed", e); + } + throw e; + } + } + + async collectionRenamer( + exportFolder: string, + collectionIDExportNameMap: Map, + renamedCollections: Collection[], + isCanceled: CancellationStatus, + ) { + const fs = ensureElectron().fs; + try { + for (const collection of renamedCollections) { + try { + if (isCanceled.status) { + throw Error(CustomError.EXPORT_STOPPED); + } + await this.verifyExportFolderExists(exportFolder); + const oldCollectionExportName = + collectionIDExportNameMap.get(collection.id); + const oldCollectionExportPath = joinPath( + exportFolder, + // @ts-ignore + oldCollectionExportName, + ); + const newCollectionExportName = await safeDirectoryName( + exportFolder, + getCollectionUserFacingName(collection), + fs.exists, + ); + log.info( + `renaming collection with id ${collection.id} from ${oldCollectionExportName} to ${newCollectionExportName}`, + ); + const newCollectionExportPath = joinPath( + exportFolder, + newCollectionExportName, + ); + await this.addCollectionExportedRecord( + exportFolder, + collection.id, + newCollectionExportName, + ); + collectionIDExportNameMap.set( + collection.id, + newCollectionExportName, + ); + try { + await fs.rename( + oldCollectionExportPath, + newCollectionExportPath, + ); + } catch (e) { + await this.addCollectionExportedRecord( + exportFolder, + collection.id, + // @ts-ignore + oldCollectionExportName, + ); + collectionIDExportNameMap.set( + collection.id, + // @ts-ignore + oldCollectionExportName, + ); + throw e; + } + log.info( + `renaming collection with id ${collection.id} from ${oldCollectionExportName} to ${newCollectionExportName} successful`, + ); + } catch (e) { + log.error("collectionRenamer failed a collection", e); + if ( + // @ts-ignore + e.message === + CustomError.UPDATE_EXPORTED_RECORD_FAILED || + // @ts-ignore + e.message === + CustomError.EXPORT_FOLDER_DOES_NOT_EXIST || + // @ts-ignore + e.message === CustomError.EXPORT_STOPPED + ) { + throw e; + } + } + } + } catch (e) { + if ( + // @ts-ignore + e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && + // @ts-ignore + e.message !== CustomError.EXPORT_STOPPED + ) { + log.error("collectionRenamer failed", e); + } + throw e; + } + } + + async collectionRemover( + deletedExportedCollectionIDs: number[], + exportFolder: string, + isCanceled: CancellationStatus, + ) { + const fs = ensureElectron().fs; + const rmdirIfExists = async (dirPath: string) => { + if (await fs.exists(dirPath)) await fs.rmdir(dirPath); + }; + try { + const exportRecord = await this.getExportRecord(exportFolder); + const collectionIDPathMap = + convertCollectionIDExportNameObjectToMap( + exportRecord.collectionExportNames, + ); + for (const collectionID of deletedExportedCollectionIDs) { + try { + if (isCanceled.status) { + throw Error(CustomError.EXPORT_STOPPED); + } + await this.verifyExportFolderExists(exportFolder); + log.info( + `removing collection with id ${collectionID} from export folder`, + ); + const collectionExportName = + collectionIDPathMap.get(collectionID); + // verify that the all exported files from the collection has been removed + const collectionExportedFiles = getCollectionExportedFiles( + exportRecord, + collectionID, + ); + if (collectionExportedFiles.length > 0) { + throw new Error( + "collection is not empty, can't remove", + ); + } + const collectionExportPath = joinPath( + exportFolder, + // @ts-ignore + collectionExportName, + ); + await this.removeCollectionExportedRecord( + exportFolder, + collectionID, + ); + try { + // delete the collection metadata folder + await rmdirIfExists( + getMetadataFolderExportPath(collectionExportPath), + ); + // delete the collection folder + await rmdirIfExists(collectionExportPath); + } catch (e) { + await this.addCollectionExportedRecord( + exportFolder, + collectionID, + // @ts-ignore + collectionExportName, + ); + throw e; + } + log.info( + `removing collection with id ${collectionID} from export folder successful`, + ); + } catch (e) { + log.error("collectionRemover failed a collection", e); + if ( + // @ts-ignore + e.message === + CustomError.UPDATE_EXPORTED_RECORD_FAILED || + // @ts-ignore + e.message === + CustomError.EXPORT_FOLDER_DOES_NOT_EXIST || + // @ts-ignore + e.message === CustomError.EXPORT_STOPPED + ) { + throw e; + } + } + } + } catch (e) { + if ( + // @ts-ignore + e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && + // @ts-ignore + e.message !== CustomError.EXPORT_STOPPED + ) { + log.error("collectionRemover failed", e); + } + throw e; + } + } + + async fileExporter( + files: EnteFile[], + collectionIDNameMap: Map, + collectionIDFolderNameMap: Map, + exportDir: string, + incrementSuccess: () => void, + incrementFailed: () => void, + isCanceled: CancellationStatus, + ): Promise { + const fs = ensureElectron().fs; + try { + for (const file of files) { + log.info( + `exporting file ${file.metadata.title} with id ${ + file.id + } from collection ${collectionIDNameMap.get( + file.collectionID, + )}`, + ); + if (isCanceled.status) { + throw Error(CustomError.EXPORT_STOPPED); + } + try { + await this.verifyExportFolderExists(exportDir); + let collectionExportName = collectionIDFolderNameMap.get( + file.collectionID, + ); + if (!collectionExportName) { + collectionExportName = + await this.createNewCollectionExport( + exportDir, + file.collectionID, + collectionIDNameMap, + ); + await this.addCollectionExportedRecord( + exportDir, + file.collectionID, + collectionExportName, + ); + collectionIDFolderNameMap.set( + file.collectionID, + collectionExportName, + ); + } + const collectionExportPath = joinPath( + exportDir, + collectionExportName, + ); + await fs.mkdirIfNeeded(collectionExportPath); + await fs.mkdirIfNeeded( + getMetadataFolderExportPath(collectionExportPath), + ); + await this.downloadAndSave( + exportDir, + collectionExportPath, + file, + ); + incrementSuccess(); + log.info( + `exporting file ${file.metadata.title} with id ${ + file.id + } from collection ${collectionIDNameMap.get( + file.collectionID, + )} successful`, + ); + } catch (e) { + incrementFailed(); + log.error("export failed for a file", e); + if ( + // @ts-ignore + e.message === + CustomError.UPDATE_EXPORTED_RECORD_FAILED || + // @ts-ignore + e.message === + CustomError.EXPORT_FOLDER_DOES_NOT_EXIST || + // @ts-ignore + e.message === CustomError.EXPORT_STOPPED + ) { + throw e; + } + } + } + } catch (e) { + if ( + // @ts-ignore + e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && + // @ts-ignore + e.message !== CustomError.EXPORT_STOPPED + ) { + log.error("fileExporter failed", e); + } + throw e; + } + } + + async fileTrasher( + exportDir: string, + collectionIDExportNameMap: Map, + removedFileUIDs: string[], + isCanceled: CancellationStatus, + ): Promise { + try { + const exportRecord = await this.getExportRecord(exportDir); + const fileIDExportNameMap = convertFileIDExportNameObjectToMap( + exportRecord.fileExportNames, + ); + for (const fileUID of removedFileUIDs) { + await this.verifyExportFolderExists(exportDir); + log.info(`trashing file with id ${fileUID}`); + if (isCanceled.status) { + throw Error(CustomError.EXPORT_STOPPED); + } + try { + const fileExportName = fileIDExportNameMap.get(fileUID); + const collectionID = getCollectionIDFromFileUID(fileUID); + const collectionExportName = + collectionIDExportNameMap.get(collectionID); + + // @ts-ignore + if (isLivePhotoExportName(fileExportName)) { + const { image, video } = + // @ts-ignore + parseLivePhotoExportName(fileExportName); + + await moveToFSTrash( + exportDir, + // @ts-ignore + collectionExportName, + image, + ); + + await moveToFSTrash( + exportDir, + // @ts-ignore + collectionExportName, + video, + ); + } else { + await moveToFSTrash( + exportDir, + // @ts-ignore + collectionExportName, + fileExportName, + ); + } + + await this.removeFileExportedRecord(exportDir, fileUID); + + log.info(`Moved file id ${fileUID} to Trash`); + } catch (e) { + log.error("trashing failed for a file", e); + if ( + // @ts-ignore + e.message === + CustomError.UPDATE_EXPORTED_RECORD_FAILED || + // @ts-ignore + e.message === + CustomError.EXPORT_FOLDER_DOES_NOT_EXIST || + // @ts-ignore + e.message === CustomError.EXPORT_STOPPED + ) { + throw e; + } + } + } + } catch (e) { + if ( + // @ts-ignore + e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && + // @ts-ignore + e.message !== CustomError.EXPORT_STOPPED + ) { + log.error("fileTrasher failed", e); + } + throw e; + } + } + + async addFileExportedRecord( + folder: string, + fileUID: string, + fileExportName: string, + ) { + try { + const exportRecord = await this.getExportRecord(folder); + if (!exportRecord.fileExportNames) { + exportRecord.fileExportNames = {}; + } + exportRecord.fileExportNames = { + ...exportRecord.fileExportNames, + [fileUID]: fileExportName, + }; + await this.updateExportRecord(folder, exportRecord); + } catch (e) { + // @ts-ignore + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + log.error("addFileExportedRecord failed", e); + } + throw e; + } + } + + async addCollectionExportedRecord( + folder: string, + collectionID: number, + collectionExportName: string, + ) { + try { + const exportRecord = await this.getExportRecord(folder); + if (!exportRecord?.collectionExportNames) { + exportRecord.collectionExportNames = {}; + } + exportRecord.collectionExportNames = { + ...exportRecord.collectionExportNames, + [collectionID]: collectionExportName, + }; + + await this.updateExportRecord(folder, exportRecord); + } catch (e) { + // @ts-ignore + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + log.error("addCollectionExportedRecord failed", e); + } + throw e; + } + } + + async removeCollectionExportedRecord(folder: string, collectionID: number) { + try { + const exportRecord = await this.getExportRecord(folder); + + exportRecord.collectionExportNames = Object.fromEntries( + Object.entries(exportRecord.collectionExportNames).filter( + ([key]) => key !== collectionID.toString(), + ), + ); + + await this.updateExportRecord(folder, exportRecord); + } catch (e) { + // @ts-ignore + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + log.error("removeCollectionExportedRecord failed", e); + } + throw e; + } + } + + async removeFileExportedRecord(folder: string, fileUID: string) { + try { + const exportRecord = await this.getExportRecord(folder); + exportRecord.fileExportNames = Object.fromEntries( + Object.entries(exportRecord.fileExportNames).filter( + ([key]) => key !== fileUID, + ), + ); + await this.updateExportRecord(folder, exportRecord); + } catch (e) { + // @ts-ignore + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + log.error("removeFileExportedRecord failed", e); + } + throw e; + } + } + + async updateExportRecord(folder: string, newData: Partial) { + return this.exportRecordUpdater.add(() => + this.updateExportRecordHelper(folder, newData), + ); + } + + async updateExportRecordHelper( + folder: string, + newData: Partial, + ) { + try { + const exportRecord = await this.getExportRecord(folder); + const newRecord: ExportRecord = { ...exportRecord, ...newData }; + await ensureElectron().fs.writeFileViaBackup( + joinPath(folder, exportRecordFileName), + JSON.stringify(newRecord, null, 2), + ); + return newRecord; + } catch (e) { + // @ts-ignore + if (e.message === CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + throw e; + } + log.error("error updating Export Record", e); + throw Error(CustomError.UPDATE_EXPORTED_RECORD_FAILED); + } + } + + async getExportRecord(folder: string): Promise { + const electron = ensureElectron(); + const fs = electron.fs; + try { + await this.verifyExportFolderExists(folder); + const exportRecordJSONPath = joinPath(folder, exportRecordFileName); + if (!(await fs.exists(exportRecordJSONPath))) { + return await this.createEmptyExportRecord(exportRecordJSONPath); + } + const recordFile = await fs.readTextFile(exportRecordJSONPath); + return JSON.parse(recordFile); + } catch (e) { + // @ts-ignore + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + log.error("export Record JSON parsing failed", e); + } + throw e; + } + } + + async createNewCollectionExport( + exportFolder: string, + collectionID: number, + collectionIDNameMap: Map, + ) { + const fs = ensureElectron().fs; + await this.verifyExportFolderExists(exportFolder); + const collectionName = collectionIDNameMap.get(collectionID); + const collectionExportName = await safeDirectoryName( + exportFolder, + // @ts-ignore + collectionName, + fs.exists, + ); + const collectionExportPath = joinPath( + exportFolder, + collectionExportName, + ); + await fs.mkdirIfNeeded(collectionExportPath); + await fs.mkdirIfNeeded( + getMetadataFolderExportPath(collectionExportPath), + ); + + return collectionExportName; + } + + async downloadAndSave( + exportDir: string, + collectionExportPath: string, + file: EnteFile, + ): Promise { + const electron = ensureElectron(); + try { + const fileUID = getExportRecordFileUID(file); + const originalFileStream = await downloadManager.fileStream(file, { + background: true, + }); + if (file.metadata.fileType === FileType.livePhoto) { + await this.exportLivePhoto( + exportDir, + fileUID, + collectionExportPath, + // @ts-ignore + originalFileStream, + file, + ); + } else { + const fileExportName = await safeFileName( + collectionExportPath, + file.metadata.title, + electron.fs.exists, + ); + await this.saveMetadataFile( + collectionExportPath, + fileExportName, + file, + ); + await writeStream( + electron, + joinPath(collectionExportPath, fileExportName), + // @ts-ignore + originalFileStream, + ); + await this.addFileExportedRecord( + exportDir, + fileUID, + fileExportName, + ); + } + } catch (e) { + log.error("download and save failed", e); + throw e; + } + } + + private async exportLivePhoto( + exportDir: string, + fileUID: string, + collectionExportPath: string, + fileStream: ReadableStream, + file: EnteFile, + ) { + const fs = ensureElectron().fs; + const fileBlob = await new Response(fileStream).blob(); + const livePhoto = await decodeLivePhoto(file.metadata.title, fileBlob); + const imageExportName = await safeFileName( + collectionExportPath, + livePhoto.imageFileName, + fs.exists, + ); + const videoExportName = await safeFileName( + collectionExportPath, + livePhoto.videoFileName, + fs.exists, + ); + + const livePhotoExportName = getLivePhotoExportName( + imageExportName, + videoExportName, + ); + + await this.saveMetadataFile( + collectionExportPath, + imageExportName, + file, + ); + await writeStream( + // @ts-ignore + electron, + joinPath(collectionExportPath, imageExportName), + new Response(livePhoto.imageData).body, + ); + + await this.saveMetadataFile( + collectionExportPath, + videoExportName, + file, + ); + try { + await writeStream( + // @ts-ignore + electron, + joinPath(collectionExportPath, videoExportName), + new Response(livePhoto.videoData).body, + ); + } catch (e) { + await fs.rm(joinPath(collectionExportPath, imageExportName)); + throw e; + } + + await this.addFileExportedRecord( + exportDir, + fileUID, + livePhotoExportName, + ); + } + + private async saveMetadataFile( + collectionExportPath: string, + fileExportName: string, + file: EnteFile, + ) { + const formatter = this.metadataDateTimeFormatter(); + await ensureElectron().fs.writeFile( + getFileMetadataExportPath(collectionExportPath, fileExportName), + getGoogleLikeMetadataFile(fileExportName, file, formatter), + ); + } + + /** + * Lazily created, cached instance of the date time formatter that should be + * used for formatting the dates added to the metadata file. + */ + private metadataDateTimeFormatter() { + if (this.cachedMetadataDateTimeFormatter) + return this.cachedMetadataDateTimeFormatter; + + // AFAIK, Google's format is not documented. It also seems to vary with + // locale. This is a best attempt at constructing a formatter that + // mirrors the format used by the timestamps in the takeout JSON. + const formatter = new Intl.DateTimeFormat(i18n.language, { + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + timeZoneName: "short", + timeZone: "UTC", + }); + this.cachedMetadataDateTimeFormatter = formatter; + return formatter; + } + + isExportInProgress = () => { + return this.exportInProgress; + }; + + exportFolderExists = async (exportFolder: string) => { + return exportFolder && (await ensureElectron().fs.exists(exportFolder)); + }; + + private verifyExportFolderExists = async (exportFolder: string) => { + try { + if (!(await this.exportFolderExists(exportFolder))) { + throw Error(CustomError.EXPORT_FOLDER_DOES_NOT_EXIST); + } + } catch (e) { + // @ts-ignore + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + log.error("verifyExportFolderExists failed", e); + } + throw e; + } + }; + + private createEmptyExportRecord = async (exportRecordJSONPath: string) => { + const exportRecord: ExportRecord = NULL_EXPORT_RECORD; + await ensureElectron().fs.writeFile( + exportRecordJSONPath, + JSON.stringify(exportRecord, null, 2), + ); + return exportRecord; + }; +} + +const exportService = new ExportService(); + +export default exportService; + +/** + * If there are any in-progress exports, or if continuous exports are enabled, + * resume them. + */ +export const resumeExportsIfNeeded = async () => { + const exportSettings = exportService.getExportSettings(); + if (!(await exportService.exportFolderExists(exportSettings?.folder))) { + return; + } + const exportRecord = await exportService.getExportRecord( + exportSettings.folder, + ); + if (exportSettings.continuousExport) { + exportService.enableContinuousExport(); + } + if (isExportInProgress(exportRecord.stage)) { + log.debug(() => "Resuming in-progress export"); + exportService.resumeExport(); + } +}; + +/** + * Prompt the user to select a directory and create an export directory in it. + * + * If the user cancels the selection, return undefined. + */ +export const selectAndPrepareExportDirectory = async (): Promise< + string | undefined +> => { + const electron = ensureElectron(); + + const rootDir = await electron.selectDirectory(); + if (!rootDir) return undefined; + + const exportDir = joinPath(rootDir, exportDirectoryName); + await electron.fs.mkdirIfNeeded(exportDir); + return exportDir; +}; + +export const getExportRecordFileUID = (file: EnteFile) => + `${file.id}_${file.collectionID}_${file.updationTime}`; + +export const getCollectionIDFromFileUID = (fileUID: string) => + Number(fileUID.split("_")[1]); + +const convertCollectionIDExportNameObjectToMap = ( + collectionExportNames: CollectionExportNames, +): Map => { + return new Map( + Object.entries(collectionExportNames ?? {}).map((e) => { + return [Number(e[0]), String(e[1])]; + }), + ); +}; + +const convertFileIDExportNameObjectToMap = ( + fileExportNames: FileExportNames, +): Map => { + return new Map( + Object.entries(fileExportNames ?? {}).map((e) => { + return [String(e[0]), String(e[1])]; + }), + ); +}; + +const getRenamedExportedCollections = ( + collections: Collection[], + exportRecord: ExportRecord, +) => { + if (!exportRecord?.collectionExportNames) { + return []; + } + const collectionIDExportNameMap = convertCollectionIDExportNameObjectToMap( + exportRecord.collectionExportNames, + ); + const renamedCollections = collections.filter((collection) => { + if (collectionIDExportNameMap.has(collection.id)) { + const currentExportName = collectionIDExportNameMap.get( + collection.id, + ); + + const collectionExportName = + getCollectionUserFacingName(collection); + + if (currentExportName === collectionExportName) { + return false; + } + // @ts-ignore + const hasNumberedSuffix = /\(\d+\)$/.exec(currentExportName); + const currentExportNameWithoutNumberedSuffix = hasNumberedSuffix + ? // @ts-ignore + currentExportName.replace(/\(\d+\)$/, "") + : currentExportName; + + return ( + collectionExportName !== currentExportNameWithoutNumberedSuffix + ); + } + return false; + }); + return renamedCollections; +}; + +const getDeletedExportedCollections = ( + collections: Collection[], + exportRecord: ExportRecord, +) => { + if (!exportRecord?.collectionExportNames) { + return []; + } + const presentCollections = new Set( + collections.map((collection) => collection.id), + ); + const deletedExportedCollections = Object.keys( + exportRecord?.collectionExportNames, + ) + .map(Number) + .filter((collectionID) => { + if (!presentCollections.has(collectionID)) { + return true; + } + return false; + }); + return deletedExportedCollections; +}; + +/** + * Return export record IDs of {@link files} for which there is also exists a + * file on disk. + */ +const readOnDiskFileExportRecordIDs = async ( + files: EnteFile[], + collectionIDFolderNameMap: Map, + exportDir: string, + exportRecord: ExportRecord, + isCanceled: CancellationStatus, +): Promise> => { + const fs = ensureElectron().fs; + + const result = new Set(); + if (!(await fs.exists(exportDir))) return result; + + // Both the paths involved are guaranteed to use POSIX separators and thus + // can directly be compared. + // + // - `exportDir` traces its origin to `electron.selectDirectory()`, which + // returns POSIX paths. Down below we use it as the base directory when + // constructing paths for the items to export. + // + // - `findFiles` is also guaranteed to return POSIX paths. + // + const ls = new Set(await ensureElectron().fs.findFiles(exportDir)); + + const fileExportNames = exportRecord.fileExportNames ?? {}; + + for (const file of files) { + if (isCanceled.status) throw Error(CustomError.EXPORT_STOPPED); + + const collectionExportName = collectionIDFolderNameMap.get( + file.collectionID, + ); + if (!collectionExportName) continue; + + const collectionExportPath = joinPath(exportDir, collectionExportName); + const recordID = getExportRecordFileUID(file); + const exportName = fileExportNames[recordID]; + if (!exportName) continue; + + if (ls.has(joinPath(collectionExportPath, exportName))) { + result.add(recordID); + } else { + // It might be a live photo - these store a JSON string instead of + // the file's name as the exportName. + try { + const { image, video } = parseLivePhotoExportName(exportName); + if ( + ls.has(joinPath(collectionExportPath, image)) && + ls.has(joinPath(collectionExportPath, video)) + ) { + result.add(recordID); + } + } catch { + /* Not an error, the file just might not exist on disk yet */ + } + } + } + + return result; +}; + +/** + * Return the list of files from amongst {@link allFiles} that still need to be + * exported. + * + * @param allFiles The list of files to export. + * + * @param exportRecord The export record containing bookkeeping for the export. + * + * @paramd diskFileRecordIDs (Optional) The export record IDs of files from + * amongst {@link allFiles} that already exist on disk. If provided (e.g. when + * doing a resync), we perform an extra check for on-disk existence instead of + * relying solely on the export record. + */ +const getUnExportedFiles = ( + allFiles: EnteFile[], + exportRecord: ExportRecord | undefined, + diskFileRecordIDs: Set | undefined, +) => { + if (!exportRecord?.fileExportNames) { + return allFiles; + } + const exportedFiles = new Set(Object.keys(exportRecord?.fileExportNames)); + return allFiles.filter((file) => { + const recordID = getExportRecordFileUID(file); + if (!exportedFiles.has(recordID)) return true; + if (diskFileRecordIDs && !diskFileRecordIDs.has(recordID)) return true; + return false; + }); +}; + +const getDeletedExportedFiles = ( + allFiles: EnteFile[], + exportRecord: ExportRecord, +): string[] => { + if (!exportRecord?.fileExportNames) { + return []; + } + const presentFileUIDs = new Set( + allFiles?.map((file) => getExportRecordFileUID(file)), + ); + const deletedExportedFiles = Object.keys( + exportRecord?.fileExportNames, + ).filter((fileUID) => { + if (!presentFileUIDs.has(fileUID)) { + return true; + } + return false; + }); + return deletedExportedFiles; +}; + +const getCollectionExportedFiles = ( + exportRecord: ExportRecord, + collectionID: number, +): string[] => { + if (!exportRecord?.fileExportNames) { + return []; + } + const collectionExportedFiles = Object.keys( + exportRecord?.fileExportNames, + ).filter((fileUID) => { + const fileCollectionID = Number(fileUID.split("_")[1]); + if (fileCollectionID === collectionID) { + return true; + } else { + return false; + } + }); + return collectionExportedFiles; +}; + +const getGoogleLikeMetadataFile = ( + fileExportName: string, + file: EnteFile, + dateTimeFormatter: Intl.DateTimeFormat, +) => { + const metadata: Metadata = file.metadata; + const publicMagicMetadata = file.pubMagicMetadata?.data; + const creationTime = Math.floor( + (publicMagicMetadata?.editedTime ?? metadata.creationTime) / 1e6, + ); + const modificationTime = Math.floor( + (metadata.modificationTime ?? metadata.creationTime) / 1e6, + ); + const result: Record = { + title: fileExportName, + creationTime: { + timestamp: `${creationTime}`, + formatted: dateTimeFormatter.format(creationTime * 1000), + }, + modificationTime: { + timestamp: `${modificationTime}`, + formatted: dateTimeFormatter.format(modificationTime * 1000), + }, + }; + const caption = file?.pubMagicMetadata?.data?.caption; + if (caption) result.caption = caption; + const geoData = fileLocation(file); + if (geoData) result.geoData = geoData; + return JSON.stringify(result, null, 2); +}; + +export const getMetadataFolderExportPath = (collectionExportPath: string) => + joinPath(collectionExportPath, exportMetadataDirectoryName); + +// if filepath is /home/user/Ente/Export/Collection1/1.jpg +// then metadata path is /home/user/Ente/Export/Collection1/ENTE_METADATA_FOLDER/1.jpg.json +const getFileMetadataExportPath = ( + collectionExportPath: string, + fileExportName: string, +) => + joinPath( + collectionExportPath, + joinPath(exportMetadataDirectoryName, `${fileExportName}.json`), + ); + +export const getLivePhotoExportName = ( + imageExportName: string, + videoExportName: string, +) => JSON.stringify({ image: imageExportName, video: videoExportName }); + +export const isLivePhotoExportName = (exportName: string) => { + try { + JSON.parse(exportName); + return true; + } catch { + return false; + } +}; + +const parseLivePhotoExportName = ( + livePhotoExportName: string, +): { image: string; video: string } => { + const { image, video } = JSON.parse(livePhotoExportName); + return { image, video }; +}; + +const isExportInProgress = (exportStage: ExportStage) => + exportStage > ExportStage.init && exportStage < ExportStage.finished; + +/** + * Move {@link fileName} in {@link collectionName} to the special per-collection + * file system "Trash" folder we created under the export directory. + * + * Also move its associated metadata JSON to Trash. + * + * @param exportDir The root directory on the user's file system where we are + * exporting to. + * */ +const moveToFSTrash = async ( + exportDir: string, + collectionName: string, + fileName: string, +) => { + const fs = ensureElectron().fs; + + const filePath = joinPath(exportDir, joinPath(collectionName, fileName)); + const trashDir = joinPath( + exportDir, + joinPath(exportTrashDirectoryName, collectionName), + ); + const metadataFileName = `${fileName}.json`; + const metadataFilePath = joinPath( + exportDir, + joinPath( + collectionName, + joinPath(exportMetadataDirectoryName, metadataFileName), + ), + ); + const metadataTrashDir = joinPath( + exportDir, + joinPath( + exportTrashDirectoryName, + joinPath(collectionName, exportMetadataDirectoryName), + ), + ); + + log.info(`Moving file ${filePath} and its metadata to trash folder`); + + if (await fs.exists(filePath)) { + await fs.mkdirIfNeeded(trashDir); + const trashFileName = await safeFileName(trashDir, fileName, fs.exists); + const trashFilePath = joinPath(trashDir, trashFileName); + await fs.rename(filePath, trashFilePath); + } + + if (await fs.exists(metadataFilePath)) { + await fs.mkdirIfNeeded(metadataTrashDir); + const metadataTrashFileName = await safeFileName( + metadataTrashDir, + metadataFileName, + fs.exists, + ); + const metadataTrashFilePath = joinPath( + metadataTrashDir, + metadataTrashFileName, + ); + await fs.rename(metadataFilePath, metadataTrashFilePath); + } +}; diff --git a/web/packages/new/photos/services/files.ts b/web/packages/new/photos/services/files.ts index a3fad3982e..56752ee480 100644 --- a/web/packages/new/photos/services/files.ts +++ b/web/packages/new/photos/services/files.ts @@ -242,6 +242,13 @@ export function getTrashedFiles(trash: Trash): EnteFile[] { ); } +/** + * Return the IDs of all the files that are part of the trash as per our local + * database. + */ +export const getLocalTrashFileIDs = () => + getLocalTrash().then((trash) => new Set(trash.map((f) => f.file.id))); + const sortTrashFiles = (files: EnteFile[]) => { return files.sort((a, b) => { if (a.deleteBy === b.deleteBy) { diff --git a/web/packages/new/photos/services/ml/blob.ts b/web/packages/new/photos/services/ml/blob.ts index 9752418dd6..0039fa4091 100644 --- a/web/packages/new/photos/services/ml/blob.ts +++ b/web/packages/new/photos/services/ml/blob.ts @@ -164,7 +164,9 @@ export const fetchRenderableEnteFileBlob = async ( return new Blob([thumbnailData!]); } - const originalFileBlob = await downloadManager.fileBlob(file); + const originalFileBlob = await downloadManager.fileBlob(file, { + background: true, + }); if (fileType == FileType.livePhoto) { const { imageFileName, imageData } = await decodeLivePhoto( diff --git a/web/packages/new/photos/services/ml/db.ts b/web/packages/new/photos/services/ml/db.ts index a8eb9398bd..a253803919 100644 --- a/web/packages/new/photos/services/ml/db.ts +++ b/web/packages/new/photos/services/ml/db.ts @@ -255,7 +255,7 @@ export const addFileEntry = async (fileID: number) => { */ export const updateAssumingLocalFiles = async ( localFileIDs: number[], - localTrashFilesIDs: number[], + localTrashFilesIDs: Set, ) => { const db = await mlDB(); const tx = db.transaction( @@ -268,14 +268,13 @@ export const updateAssumingLocalFiles = async ( .getAllKeys(IDBKeyRange.only("indexed")); const local = new Set(localFileIDs); - const localTrash = new Set(localTrashFilesIDs); const fdb = new Set(fdbFileIDs); const fdbIndexed = new Set(fdbIndexedFileIDs); const newFileIDs = localFileIDs.filter((id) => !fdb.has(id)); const removedFileIDs = fdbFileIDs.filter((id) => { if (local.has(id)) return false; // Still exists. - if (localTrash.has(id)) { + if (localTrashFilesIDs.has(id)) { // Exists in trash. if (fdbIndexed.has(id)) { // But is already indexed, so let it be. diff --git a/web/packages/new/photos/services/ml/index.ts b/web/packages/new/photos/services/ml/index.ts index f97d9d91bd..f02078a99b 100644 --- a/web/packages/new/photos/services/ml/index.ts +++ b/web/packages/new/photos/services/ml/index.ts @@ -471,10 +471,16 @@ export const mlStatusSubscribe = (onChange: () => void): (() => void) => { * * See also {@link mlStatusSubscribe}. * + * This function can be safely called even if {@link isMLSupported} is `false` + * (in such cases, it will always return `undefined`). This is so that it can be + * unconditionally called as part of a React hook. + * * A return value of `undefined` indicates that we're still performing the * asynchronous tasks that are needed to get the status. */ export const mlStatusSnapshot = (): MLStatus | undefined => { + if (!isMLSupported) return undefined; + const result = _state.mlStatusSnapshot; // We don't have it yet, trigger an update. if (!result) triggerStatusUpdate(); diff --git a/web/packages/new/photos/services/ml/ml-data.ts b/web/packages/new/photos/services/ml/ml-data.ts index f2257369ac..228c915a88 100644 --- a/web/packages/new/photos/services/ml/ml-data.ts +++ b/web/packages/new/photos/services/ml/ml-data.ts @@ -56,6 +56,8 @@ import { type RemoteFaceIndex } from "./face"; export interface RemoteMLData { raw: RawRemoteMLData; parsed: ParsedRemoteMLData | undefined; + // See: [Note: PUT "mldata" version check] + updatedAt: number | undefined; } export type RawRemoteMLData = Record; @@ -159,7 +161,7 @@ export const fetchMLData = async ( const result = new Map(); for (const remoteFileData of remoteFileDatas) { - const { fileID } = remoteFileData; + const { fileID, updatedAt } = remoteFileData; const file = filesByID.get(fileID); if (!file) { log.warn(`Ignoring ML data for unknown file id ${fileID}`); @@ -173,7 +175,10 @@ export const fetchMLData = async ( // @ts-ignore const decryptedBytes = await decryptBlob(remoteFileData, file.key); const jsonString = await gunzip(decryptedBytes); - result.set(fileID, remoteMLDataFromJSONString(jsonString)); + result.set( + fileID, + remoteMLDataFromJSONString(jsonString, updatedAt), + ); } catch (e) { // This shouldn't happen. Best guess is that some client has // uploaded a corrupted ML index. Ignore it so that it gets @@ -185,7 +190,10 @@ export const fetchMLData = async ( return result; }; -const remoteMLDataFromJSONString = (jsonString: string) => { +const remoteMLDataFromJSONString = ( + jsonString: string, + updatedAt: number | undefined, +) => { const raw = RawRemoteMLData.parse(JSON.parse(jsonString)); const parseResult = ParsedRemoteMLData.safeParse(raw); // TODO: [Note: strict mode migration] @@ -199,7 +207,7 @@ const remoteMLDataFromJSONString = (jsonString: string) => { const parsed = parseResult.success ? (parseResult.data as ParsedRemoteMLData) : undefined; - return { raw, parsed }; + return { raw, parsed, updatedAt }; }; /** @@ -214,5 +222,14 @@ const remoteMLDataFromJSONString = (jsonString: string) => { * * See: [Note: Preserve unknown ML data fields]. */ -export const putMLData = async (file: EnteFile, mlData: RawRemoteMLData) => - putFileData(file, "mldata", await gzip(JSON.stringify(mlData))); +export const putMLData = async ( + file: EnteFile, + mlData: RawRemoteMLData, + lastUpdatedAt: number, +) => + putFileData( + file, + "mldata", + await gzip(JSON.stringify(mlData)), + lastUpdatedAt, + ); diff --git a/web/packages/new/photos/services/ml/worker.ts b/web/packages/new/photos/services/ml/worker.ts index f3138ad639..28ecbd09a8 100644 --- a/web/packages/new/photos/services/ml/worker.ts +++ b/web/packages/new/photos/services/ml/worker.ts @@ -1,7 +1,7 @@ import { expose, wrap } from "comlink"; -import { clientPackageName } from "ente-base/app"; +import { clientIdentifier } from "ente-base/app"; import { assertionFailed } from "ente-base/assert"; -import { isHTTP4xxError } from "ente-base/http"; +import { isHTTP4xxError, isHTTPErrorWithStatus } from "ente-base/http"; import log from "ente-base/log"; import { logUnhandledErrorsAndRejectionsInWorker } from "ente-base/log-web"; import type { ElectronMLWorker } from "ente-base/types/ipc"; @@ -9,7 +9,7 @@ import { isNetworkDownloadError } from "ente-gallery/services/download"; import type { ProcessableUploadItem } from "ente-gallery/services/upload"; import { fileLogID, type EnteFile } from "ente-media/file"; import { wait } from "ente-utils/promise"; -import { getAllLocalFiles, getLocalTrashedFiles } from "../files"; +import { getAllLocalFiles, getLocalTrashFileIDs } from "../files"; import { createImageBitmapAndData, fetchRenderableBlob, @@ -438,11 +438,9 @@ const syncWithLocalFilesAndGetFilesToIndex = async ( const localFiles = await getAllLocalFiles(); const localFileByID = new Map(localFiles.map((f) => [f.id, f])); - const localTrashFileIDs = (await getLocalTrashedFiles()).map((f) => f.id); - await updateAssumingLocalFiles( Array.from(localFileByID.keys()), - localTrashFileIDs, + await getLocalTrashFileIDs(), ); const fileIDsToIndex = await getIndexableFileIDs(count); @@ -592,13 +590,13 @@ const index = async ( const remoteFaceIndex = existingRemoteFaceIndex ?? { version: faceIndexingVersion, - client: clientPackageName, + client: clientIdentifier, ...faceIndex, }; const remoteCLIPIndex = existingRemoteCLIPIndex ?? { version: clipIndexingVersion, - client: clientPackageName, + client: clientIdentifier, ...clipIndex, }; @@ -616,10 +614,18 @@ const index = async ( log.debug(() => ["Uploading ML data", rawMLData]); try { - await putMLData(file, rawMLData); + const lastUpdatedAt = remoteMLData?.updatedAt ?? 0; + await putMLData(file, rawMLData, lastUpdatedAt); } catch (e) { // See: [Note: Transient and permanent indexing failures] - if (isHTTP4xxError(e)) await markIndexingFailed(fileID); + if (isHTTP4xxError(e)) { + // 409 Conflict indicates that we tried overwriting existing + // mldata. Don't mark it as a failure, the file has already been + // processed. + if (!isHTTPErrorWithStatus(e, 409)) { + await markIndexingFailed(fileID); + } + } throw e; } diff --git a/web/packages/new/photos/services/search/worker.ts b/web/packages/new/photos/services/search/worker.ts index 3f8529d548..d6be385a00 100644 --- a/web/packages/new/photos/services/search/worker.ts +++ b/web/packages/new/photos/services/search/worker.ts @@ -85,9 +85,10 @@ export class SearchWorker { ) { return suggestionsForString( s, - // Case insensitive word prefix match, considering underscores also - // as a word separator. - new RegExp("(\\b|_)" + s, "i"), + // Case insensitive word prefix match. Note that \b doesn't work + // with unicode characters, so we use instead a set of common + // punctuation (and spaces) to discern the word boundary. + new RegExp("(^|[\\s.,!?\"'-_])" + s, "i"), searchString, this.collectionsAndFiles, this.people, diff --git a/web/packages/new/photos/services/sync.ts b/web/packages/new/photos/services/sync.ts index 9bcdc6094d..c242b9db5e 100644 --- a/web/packages/new/photos/services/sync.ts +++ b/web/packages/new/photos/services/sync.ts @@ -1,5 +1,8 @@ import { resetFileViewerDataSourceOnClose } from "ente-gallery/components/viewer/data-source"; -import { videoProcessingSyncIfNeeded } from "ente-gallery/services/video"; +import { + videoProcessingSyncIfNeeded, + videoPrunePermanentlyDeletedFileIDsIfNeeded, +} from "ente-gallery/services/video"; import type { Collection } from "ente-media/collection"; import type { EnteFile } from "ente-media/file"; import { isHiddenCollection } from "ente-new/photos/services/collection"; @@ -141,7 +144,11 @@ export const syncCollectionAndFiles = async ( opts?.onResetHiddenFiles, opts?.onFetchHiddenFiles, ); - await syncTrash(collections, opts?.onResetTrashedFiles); + await syncTrash( + collections, + opts?.onResetTrashedFiles, + videoPrunePermanentlyDeletedFileIDsIfNeeded, + ); if (didUpdateNormalFiles || didUpdateHiddenFiles) { // TODO: Ok for now since its is only commented for the deduper (gallery // does this on the return value), but still needs fixing instead of a diff --git a/web/packages/new/photos/services/user-details.ts b/web/packages/new/photos/services/user-details.ts index 14716f3aed..119c9e98cf 100644 --- a/web/packages/new/photos/services/user-details.ts +++ b/web/packages/new/photos/services/user-details.ts @@ -259,7 +259,7 @@ export const syncUserDetails = async () => { }; /** - * Fetch user details from remote. + * Fetch the user details for the currently logged in user from remote. */ export const getUserDetailsV2 = async () => { const res = await fetch(await apiURL("/users/details/v2"), { diff --git a/web/packages/new/photos/services/user.ts b/web/packages/new/photos/services/user.ts index 0817446499..070548f652 100644 --- a/web/packages/new/photos/services/user.ts +++ b/web/packages/new/photos/services/user.ts @@ -3,6 +3,23 @@ import { apiURL } from "ente-base/origins"; import { nullToUndefined } from "ente-utils/transform"; import { z } from "zod"; +/** + * Fetch the public key from remote for the user (if any) who has registered + * with remote with the given {@link email}. + * + * @returns the base64 encoded public key of the user with {@link email}. + */ +export const getPublicKey = async (email: string) => { + const params = new URLSearchParams({ email }); + const url = await apiURL("/users/public-key"); + const res = await fetch(`${url}?${params.toString()}`, { + headers: await authenticatedRequestHeaders(), + }); + ensureOk(res); + return z.object({ publicKey: z.string() }).parse(await res.json()) + .publicKey; +}; + /** * Fetch the two-factor status (whether or not it is enabled) from remote. */ @@ -31,7 +48,7 @@ const DeleteChallengeResponse = z.object({ allowDelete: z.boolean(), // An encrypted challenge that the client needs to decrypt and provide in // the actual account deletion request. - encryptedChallenge: z.string().nullable().transform(nullToUndefined), + encryptedChallenge: z.string().nullish().transform(nullToUndefined), }); export const getAccountDeleteChallenge = async () => { diff --git a/web/packages/new/photos/utils/native-fs.ts b/web/packages/new/photos/utils/native-fs.ts index 1c0efe7b8b..fd8090d87c 100644 --- a/web/packages/new/photos/utils/native-fs.ts +++ b/web/packages/new/photos/utils/native-fs.ts @@ -9,7 +9,7 @@ import { joinPath, nameAndExtension } from "ente-base/file-name"; import { exportMetadataDirectoryName, exportTrashDirectoryName, -} from "ente-new/photos/services/export"; +} from "ente-gallery/export-dirs"; import sanitize from "sanitize-filename"; /** diff --git a/web/packages/shared/components/Container.tsx b/web/packages/shared/components/Container.tsx index ba72457080..6d6f935d7e 100644 --- a/web/packages/shared/components/Container.tsx +++ b/web/packages/shared/components/Container.tsx @@ -1,43 +1,7 @@ import { Box, styled } from "@mui/material"; -export const VerticallyCentered = styled(Box)` - flex: 1; - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - text-align: center; - overflow: auto; -`; - export const FlexWrapper = styled(Box)` display: flex; width: 100%; align-items: center; `; - -/** - * Deprecated, use {@link SpacedRow} from ente-base/components/mui/container - * instead - */ -export const SpaceBetweenFlex = styled(FlexWrapper)` - justify-content: space-between; -`; - -/** - * Deprecated, use {@link CenteredRow} from ente-base/components/mui/container - * instead - */ -export const CenteredFlex = styled(FlexWrapper)` - justify-content: center; -`; - -/** Deprecated */ -export const FluidContainer = styled(FlexWrapper)` - flex: 1; -`; - -export const VerticallyCenteredFlex = styled(Box)({ - display: "flex", - alignItems: "center", -}); diff --git a/web/packages/shared/components/Form/ShowHidePassword.tsx b/web/packages/shared/components/Form/ShowHidePassword.tsx deleted file mode 100644 index 0f3a7d3b5c..0000000000 --- a/web/packages/shared/components/Form/ShowHidePassword.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import VisibilityIcon from "@mui/icons-material/Visibility"; -import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; -import { IconButton, InputAdornment } from "@mui/material"; -import React from "react"; - -interface Iprops { - showPassword: boolean; - handleClickShowPassword: () => void; - handleMouseDownPassword: ( - event: React.MouseEvent, - ) => void; -} -const ShowHidePassword = ({ - showPassword, - handleClickShowPassword, - handleMouseDownPassword, -}: Iprops) => ( - - - {showPassword ? : } - - -); - -export default ShowHidePassword; diff --git a/web/packages/shared/components/SingleInputForm.tsx b/web/packages/shared/components/SingleInputForm.tsx index ba9d2de6d2..e05627e40d 100644 --- a/web/packages/shared/components/SingleInputForm.tsx +++ b/web/packages/shared/components/SingleInputForm.tsx @@ -2,11 +2,11 @@ import { FormHelperText } from "@mui/material"; import TextField from "@mui/material/TextField"; import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; import { LoadingButton } from "ente-base/components/mui/LoadingButton"; +import { ShowHidePasswordInputAdornment } from "ente-base/components/mui/PasswordInputAdornment"; import { FlexWrapper } from "ente-shared/components/Container"; -import ShowHidePassword from "ente-shared/components/Form/ShowHidePassword"; import { Formik, type FormikHelpers, type FormikState } from "formik"; import { t } from "i18next"; -import React, { useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import * as Yup from "yup"; interface formValues { @@ -64,6 +64,11 @@ export default function SingleInputForm(props: SingleInputFormProps) { const [loading, SetLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); + const handleToggleShowHidePassword = useCallback( + () => setShowPassword((show) => !show), + [], + ); + const submitForm = async ( values: formValues, { setFieldError, resetForm }: FormikHelpers, @@ -77,16 +82,6 @@ export default function SingleInputForm(props: SingleInputFormProps) { SetLoading(false); }; - const handleClickShowPassword = () => { - setShowPassword(!showPassword); - }; - - const handleMouseDownPassword = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - }; - const validationSchema = useMemo(() => { switch (props.fieldType) { case "text": @@ -148,14 +143,9 @@ export default function SingleInputForm(props: SingleInputFormProps) { : "on", endAdornment: props.fieldType === "password" && ( - ), }, diff --git a/web/packages/shared/error/index.ts b/web/packages/shared/error/index.ts index ca120ad44c..348cc94676 100644 --- a/web/packages/shared/error/index.ts +++ b/web/packages/shared/error/index.ts @@ -21,15 +21,21 @@ export function isApiErrorResponse(object: any): object is ApiErrorResponse { return object && "code" in object && "message" in object; } +/** + * Constant string values used mark custom errors that we need to subsequently + * catch and identify to deal with in a particular manner. + */ +export const CustomErrorMessage = { + eTagMissing: "ETag header not present in response", +}; + export const CustomError = { ETAG_MISSING: "no header/etag present in response body", - KEY_MISSING: "encrypted key missing from localStorage", FILE_TOO_LARGE: "file too large", SUBSCRIPTION_EXPIRED: "subscription expired", STORAGE_QUOTA_EXCEEDED: "storage quota exceeded", SESSION_EXPIRED: "session expired", TOKEN_EXPIRED: "token expired", - TOKEN_MISSING: "token missing", TOO_MANY_REQUESTS: "too many requests", BAD_REQUEST: "bad request", SUBSCRIPTION_NEEDED: "subscription not present", @@ -41,7 +47,6 @@ export const CustomError = { UPDATE_EXPORTED_RECORD_FAILED: "update file exported record failed", EXPORT_STOPPED: "export stopped", EXPORT_FOLDER_DOES_NOT_EXIST: "export folder does not exist", - AUTH_KEY_NOT_FOUND: "auth key not found", TWO_FACTOR_ENABLED: "two factor enabled", }; diff --git a/web/packages/shared/storage/localStorage/index.ts b/web/packages/shared/storage/localStorage/index.ts index 0e167615a0..ec8491e04d 100644 --- a/web/packages/shared/storage/localStorage/index.ts +++ b/web/packages/shared/storage/localStorage/index.ts @@ -65,7 +65,7 @@ export const setLSUser = async (user: object) => { export const migrateKVToken = async (user: unknown) => { // Throw an error if the data is in local storage but not in IndexedDB. This // is a pre-cursor to inlining this code. - // TODO(REL): Remove this sanity check after a few days. + // TODO: Remove this sanity check eventually when this code is revisited. const oldLSUser = getData("user"); const wasMissing = oldLSUser && diff --git a/web/packages/shared/storage/sessionStorage/index.ts b/web/packages/shared/storage/sessionStorage/index.ts index 7fbef7381a..bc340bbbe6 100644 --- a/web/packages/shared/storage/sessionStorage/index.ts +++ b/web/packages/shared/storage/sessionStorage/index.ts @@ -9,5 +9,3 @@ export const getKey = (key: SessionKey) => { }; export const removeKey = (key: SessionKey) => sessionStorage.removeItem(key); - -export const clearKeys = () => sessionStorage.clear(); diff --git a/web/packages/shared/user/index.ts b/web/packages/shared/user/index.ts index ccd688ee3d..c20377a3dd 100644 --- a/web/packages/shared/user/index.ts +++ b/web/packages/shared/user/index.ts @@ -1,21 +1,19 @@ import { sharedCryptoWorker } from "ente-base/crypto"; import type { B64EncryptionResult } from "ente-base/crypto/libsodium"; -import { CustomError } from "ente-shared/error"; import { getKey } from "ente-shared/storage/sessionStorage"; +/** + * Deprecated, use {@link masterKeyFromSessionIfLoggedIn} instead. + */ export const getActualKey = async () => { - try { - const encryptionKeyAttributes: B64EncryptionResult = - getKey("encryptionKey"); + const encryptionKeyAttributes: B64EncryptionResult = + getKey("encryptionKey"); - const cryptoWorker = await sharedCryptoWorker(); - const key = await cryptoWorker.decryptB64( - encryptionKeyAttributes.encryptedData, - encryptionKeyAttributes.nonce, - encryptionKeyAttributes.key, - ); - return key; - } catch { - throw new Error(CustomError.KEY_MISSING); - } + const cryptoWorker = await sharedCryptoWorker(); + const key = await cryptoWorker.decryptB64( + encryptionKeyAttributes.encryptedData, + encryptionKeyAttributes.nonce, + encryptionKeyAttributes.key, + ); + return key; }; diff --git a/web/yarn.lock b/web/yarn.lock index b81cfbcc00..ef43afabed 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -151,13 +151,18 @@ dependencies: "@babel/helper-plugin-utils" "^7.25.9" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.25.0", "@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.26.10", "@babel/runtime@^7.27.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.25.0", "@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.27.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.1.tgz#9fce313d12c9a77507f264de74626e87fd0dc541" + integrity sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog== + "@babel/template@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" @@ -457,13 +462,20 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz#c8e119a30a7c8d60b9d2e22d2073722dde3b710b" integrity sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ== -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": +"@eslint-community/eslint-utils@^4.2.0": version "4.4.1" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56" integrity sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA== dependencies: eslint-visitor-keys "^3.4.3" +"@eslint-community/eslint-utils@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a" + integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw== + dependencies: + eslint-visitor-keys "^3.4.3" + "@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1": version "4.12.1" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" @@ -483,10 +495,10 @@ resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.2.1.tgz#26042c028d1beee5ce2235a7929b91c52651646d" integrity sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw== -"@eslint/core@^0.13.0": - version "0.13.0" - resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.13.0.tgz#bf02f209846d3bf996f9e8009db62df2739b458c" - integrity sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw== +"@eslint/core@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.14.0.tgz#326289380968eaf7e96f364e1e4cf8f3adf2d003" + integrity sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg== dependencies: "@types/json-schema" "^7.0.15" @@ -505,22 +517,22 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.25.1", "@eslint/js@^9.25.1": - version "9.25.1" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.25.1.tgz#25f5c930c2b68b5ebe7ac857f754cbd61ef6d117" - integrity sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg== +"@eslint/js@9.27.0", "@eslint/js@^9.27.0": + version "9.27.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.27.0.tgz#181a23460877c484f6dd03890f4e3fa2fdeb8ff0" + integrity sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA== "@eslint/object-schema@^2.1.6": version "2.1.6" resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f" integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA== -"@eslint/plugin-kit@^0.2.8": - version "0.2.8" - resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz#47488d8f8171b5d4613e833313f3ce708e3525f8" - integrity sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA== +"@eslint/plugin-kit@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz#b71b037b2d4d68396df04a8c35a49481e5593067" + integrity sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w== dependencies: - "@eslint/core" "^0.13.0" + "@eslint/core" "^0.14.0" levn "^0.4.1" "@ffmpeg/ffmpeg@^0.12.15": @@ -839,10 +851,10 @@ prop-types "^15.8.1" react-is "^19.0.0" -"@mui/x-date-pickers@^7.29.2": - version "7.29.2" - resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.29.2.tgz#cf2cd50d508585de984028243d9baa276466ca65" - integrity sha512-GRbLE31bBktJptittYk5L5SdwOOcDdVAIrXfyYYQ6xq8CR69Sbi/y6WkSDL+FDuymM/UQa7f5UmHb6r81aDVWA== +"@mui/x-date-pickers@^7.29.3": + version "7.29.3" + resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.29.3.tgz#7e8939d5fbc1626c5d6fd02d0b8107ad010d0c3a" + integrity sha512-/A0/8fpLnEFeJKr5YQsI8jqlWPJlOtgfCGcqXHVDOLxgV3lW49+Kh5TZAc1yi6HKT3AG6k4DkNwTuu/RjJeMFA== dependencies: "@babel/runtime" "^7.25.7" "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0" @@ -860,50 +872,50 @@ "@babel/runtime" "^7.25.7" "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0" -"@next/env@15.3.1": - version "15.3.1" - resolved "https://registry.yarnpkg.com/@next/env/-/env-15.3.1.tgz#fca98dcb90d92d555972cdbf03adf9aa982e2115" - integrity sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ== +"@next/env@15.3.2": + version "15.3.2" + resolved "https://registry.yarnpkg.com/@next/env/-/env-15.3.2.tgz#7143eafa9b11cfdf3d3c7318b0facb9dfdb2948f" + integrity sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g== -"@next/swc-darwin-arm64@15.3.1": - version "15.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.1.tgz#8f9589aed9f6816687440aa36a86376b3a16af58" - integrity sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w== +"@next/swc-darwin-arm64@15.3.2": + version "15.3.2" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.2.tgz#1a7b36bf3c439f899065c878a580bc57a3630ec7" + integrity sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g== -"@next/swc-darwin-x64@15.3.1": - version "15.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.1.tgz#2df013226d848394ed7307188c141f0e6da4ab3e" - integrity sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g== +"@next/swc-darwin-x64@15.3.2": + version "15.3.2" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.2.tgz#3742026344f49128cf1b0f43814c67e880db7361" + integrity sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w== -"@next/swc-linux-arm64-gnu@15.3.1": - version "15.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.1.tgz#d1c4e24b2b27c36a7ebc21ae0573e9e98f794143" - integrity sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ== +"@next/swc-linux-arm64-gnu@15.3.2": + version "15.3.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.2.tgz#fb29d45c034e3d2eef89b0e2801d62eb86155823" + integrity sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA== -"@next/swc-linux-arm64-musl@15.3.1": - version "15.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.1.tgz#bce27533f9f046800f850a9c20832e8c15b10955" - integrity sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg== +"@next/swc-linux-arm64-musl@15.3.2": + version "15.3.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.2.tgz#396784ef312666600ab1ae481e34cb1f6e3ae730" + integrity sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg== -"@next/swc-linux-x64-gnu@15.3.1": - version "15.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.1.tgz#f90558d93bc25e01b0b271725e291863286753c4" - integrity sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A== +"@next/swc-linux-x64-gnu@15.3.2": + version "15.3.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.2.tgz#ac01fda376878e02bc6b57d1e88ab8ceae9f868e" + integrity sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg== -"@next/swc-linux-x64-musl@15.3.1": - version "15.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.1.tgz#639f143bd0f3fd6e1bde4b383dc6cd8a8ff12628" - integrity sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA== +"@next/swc-linux-x64-musl@15.3.2": + version "15.3.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.2.tgz#327a5023003bcb3ca436efc08733f091bba2b1e8" + integrity sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w== -"@next/swc-win32-arm64-msvc@15.3.1": - version "15.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.1.tgz#52ee1e63b192fec8f0230caf839cfc308d0d44d1" - integrity sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw== +"@next/swc-win32-arm64-msvc@15.3.2": + version "15.3.2" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.2.tgz#ce3a6588bd9c020960704011ab20bd0440026965" + integrity sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ== -"@next/swc-win32-x64-msvc@15.3.1": - version "15.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.1.tgz#df5ceb9c3b97bf0d61cb6e84f79bbf9e91a89d29" - integrity sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ== +"@next/swc-win32-x64-msvc@15.3.2": + version "15.3.2" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.2.tgz#43cc36097ac27639e9024a5ceaa6e7727fa968c8" + integrity sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA== "@noble/hashes@^1.2.0": version "1.7.0" @@ -931,16 +943,21 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@pkgr/core@^0.1.0": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" - integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== +"@pkgr/core@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.4.tgz#d897170a2b0ba51f78a099edccd968f7b103387c" + integrity sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw== "@popperjs/core@^2.11.8": version "2.11.8" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== +"@rolldown/pluginutils@1.0.0-beta.9": + version "1.0.0-beta.9" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz#68ef9fff5a9791a642cea0dc4380ce6cb487a84a" + integrity sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w== + "@rollup/rollup-android-arm-eabi@4.40.1": version "4.40.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz#e1562d360bca73c7bef6feef86098de3a2f1d442" @@ -1041,7 +1058,7 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz#8078b71fe0d5825dcbf83d52a7dc858b39da165c" integrity sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA== -"@stripe/stripe-js@^1.17.0": +"@stripe/stripe-js@1.54.2": version "1.54.2" resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.54.2.tgz#0665848e22cbda936cfd05256facdfbba121438d" integrity sha512-R1PwtDvUfs99cAjfuQ/WpwJ3c92+DAMy9xGApjqlWQMj0FKQabUAys2swfTRNzuYAYJh7NqK2dzcYVNkKLEKUg== @@ -1134,10 +1151,10 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/leaflet@^1.9.17": - version "1.9.17" - resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.9.17.tgz#f7f12f9306029df48801cd830f66597d614a2e08" - integrity sha512-IJ4K6t7I3Fh5qXbQ1uwL3CFVbCi6haW9+53oLWgdKlLP7EaS21byWFJxxqOx9y8I0AP0actXSJLVMbyvxhkUTA== +"@types/leaflet@^1.9.18": + version "1.9.18" + resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.9.18.tgz#8c295912419a9df21917b4380310f66e19c46cab" + integrity sha512-ht2vsoPjezor5Pmzi5hdsA7F++v5UGq9OlUduWHmMZiuQGIpJ2WS5+Gg9HaAA79gNh1AIPtCqhzejcIZ3lPzXQ== dependencies: "@types/geojson" "*" @@ -1153,10 +1170,10 @@ resolved "https://registry.yarnpkg.com/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.14.tgz#f688f8d44e46ed61c401f82ff757581655fbcc42" integrity sha512-5Kv68fXuXK0iDuUir1WPGw2R9fOZUlYlSAa0ztMcL0s0BfIDTqg9GXz8K30VJpPP3sxWhbolnQma2x+/TfkzDQ== -"@types/node@^22.15.3": - version "22.15.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.3.tgz#b7fb9396a8ec5b5dfb1345d8ac2502060e9af68b" - integrity sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw== +"@types/node@^22.15.21": + version "22.15.21" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.21.tgz#196ef14fe20d87f7caf1e7b39832767f9a995b77" + integrity sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ== dependencies: undici-types "~6.21.0" @@ -1170,7 +1187,7 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2" integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== -"@types/react-dom@^19.1.1", "@types/react-dom@^19.1.3": +"@types/react-dom@^19.1.1", "@types/react-dom@^19.1.5": version "19.1.1" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.1.tgz#a8d097b28247d1129cf56e74d1622c98978c04ed" integrity sha512-jFf/woGTVTjUJsl2O7hcopJ1r0upqoq/vIOoCj0yLh3RIXxWcljlpuZ+vEBRXsymD1jhfeJrlyTy/S1UW+4y1w== @@ -1187,7 +1204,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^19.1.0", "@types/react@^19.1.2": +"@types/react@*", "@types/react@^19.1.0", "@types/react@^19.1.5": version "19.1.0" resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.0.tgz#73c43ad9bc43496ca8184332b111e2aef63fc9da" integrity sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w== @@ -1199,85 +1216,85 @@ resolved "https://registry.yarnpkg.com/@types/zxcvbn/-/zxcvbn-4.4.5.tgz#8ce8623ed7a36e3a76d1c0b539708dfb2e859bc0" integrity sha512-FZJgC5Bxuqg7Rhsm/bx6gAruHHhDQ55r+s0JhDh8CQ16fD7NsJJ+p8YMMQDhSQoIrSmjpqqYWA96oQVMNkjRyA== -"@typescript-eslint/eslint-plugin@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz#62f1befe59647524994e89de4516d8dcba7a850a" - integrity sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ== +"@typescript-eslint/eslint-plugin@8.32.1": + version "8.32.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz#9185b3eaa3b083d8318910e12d56c68b3c4f45b4" + integrity sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.31.1" - "@typescript-eslint/type-utils" "8.31.1" - "@typescript-eslint/utils" "8.31.1" - "@typescript-eslint/visitor-keys" "8.31.1" + "@typescript-eslint/scope-manager" "8.32.1" + "@typescript-eslint/type-utils" "8.32.1" + "@typescript-eslint/utils" "8.32.1" + "@typescript-eslint/visitor-keys" "8.32.1" graphemer "^1.4.0" - ignore "^5.3.1" + ignore "^7.0.0" natural-compare "^1.4.0" - ts-api-utils "^2.0.1" + ts-api-utils "^2.1.0" -"@typescript-eslint/parser@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.31.1.tgz#e9b0ccf30d37dde724ee4d15f4dbc195995cce1b" - integrity sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q== +"@typescript-eslint/parser@8.32.1": + version "8.32.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.32.1.tgz#18b0e53315e0bc22b2619d398ae49a968370935e" + integrity sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg== dependencies: - "@typescript-eslint/scope-manager" "8.31.1" - "@typescript-eslint/types" "8.31.1" - "@typescript-eslint/typescript-estree" "8.31.1" - "@typescript-eslint/visitor-keys" "8.31.1" + "@typescript-eslint/scope-manager" "8.32.1" + "@typescript-eslint/types" "8.32.1" + "@typescript-eslint/typescript-estree" "8.32.1" + "@typescript-eslint/visitor-keys" "8.32.1" debug "^4.3.4" -"@typescript-eslint/scope-manager@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz#1eb52e76878f545e4add142e0d8e3e97e7aa443b" - integrity sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw== +"@typescript-eslint/scope-manager@8.32.1": + version "8.32.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz#9a6bf5fb2c5380e14fe9d38ccac6e4bbe17e8afc" + integrity sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA== dependencies: - "@typescript-eslint/types" "8.31.1" - "@typescript-eslint/visitor-keys" "8.31.1" + "@typescript-eslint/types" "8.32.1" + "@typescript-eslint/visitor-keys" "8.32.1" -"@typescript-eslint/type-utils@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz#be0f438fb24b03568e282a0aed85f776409f970c" - integrity sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA== +"@typescript-eslint/type-utils@8.32.1": + version "8.32.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz#b9292a45f69ecdb7db74d1696e57d1a89514d21e" + integrity sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA== dependencies: - "@typescript-eslint/typescript-estree" "8.31.1" - "@typescript-eslint/utils" "8.31.1" + "@typescript-eslint/typescript-estree" "8.32.1" + "@typescript-eslint/utils" "8.32.1" debug "^4.3.4" - ts-api-utils "^2.0.1" + ts-api-utils "^2.1.0" -"@typescript-eslint/types@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.31.1.tgz#478ed6f7e8aee1be7b63a60212b6bffe1423b5d4" - integrity sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ== +"@typescript-eslint/types@8.32.1": + version "8.32.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.32.1.tgz#b19fe4ac0dc08317bae0ce9ec1168123576c1d4b" + integrity sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg== -"@typescript-eslint/typescript-estree@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz#37792fe7ef4d3021c7580067c8f1ae66daabacdf" - integrity sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag== +"@typescript-eslint/typescript-estree@8.32.1": + version "8.32.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz#9023720ca4ecf4f59c275a05b5fed69b1276face" + integrity sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg== dependencies: - "@typescript-eslint/types" "8.31.1" - "@typescript-eslint/visitor-keys" "8.31.1" + "@typescript-eslint/types" "8.32.1" + "@typescript-eslint/visitor-keys" "8.32.1" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" minimatch "^9.0.4" semver "^7.6.0" - ts-api-utils "^2.0.1" + ts-api-utils "^2.1.0" -"@typescript-eslint/utils@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.31.1.tgz#5628ea0393598a0b2f143d0fc6d019f0dee9dd14" - integrity sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ== +"@typescript-eslint/utils@8.32.1": + version "8.32.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.32.1.tgz#4d6d5d29b9e519e9a85e9a74e9f7bdb58abe9704" + integrity sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA== dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.31.1" - "@typescript-eslint/types" "8.31.1" - "@typescript-eslint/typescript-estree" "8.31.1" + "@eslint-community/eslint-utils" "^4.7.0" + "@typescript-eslint/scope-manager" "8.32.1" + "@typescript-eslint/types" "8.32.1" + "@typescript-eslint/typescript-estree" "8.32.1" -"@typescript-eslint/visitor-keys@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz#6742b0e3ba1e0c1e35bdaf78c03e759eb8dd8e75" - integrity sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw== +"@typescript-eslint/visitor-keys@8.32.1": + version "8.32.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz#4321395cc55c2eb46036cbbb03e101994d11ddca" + integrity sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w== dependencies: - "@typescript-eslint/types" "8.31.1" + "@typescript-eslint/types" "8.32.1" eslint-visitor-keys "^4.2.0" "@vercel/edge@^1.2.1": @@ -1285,14 +1302,15 @@ resolved "https://registry.yarnpkg.com/@vercel/edge/-/edge-1.2.1.tgz#d74298f01912ea2940546f4b7639dc8619f4c89e" integrity sha512-1++yncEyIAi68D3UEOlytYb1IUcIulMWdoSzX2h9LuSeeyR7JtaIgR8DcTQ6+DmYOQn+5MCh6LY+UmK6QBByNA== -"@vitejs/plugin-react@^4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz#d7d1e9c9616d7536b0953637edfee7c6cbe2fe0f" - integrity sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w== +"@vitejs/plugin-react@^4.5.0": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz#ef2bad6be3031af2b2105b7ab2754f710e890a32" + integrity sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg== dependencies: "@babel/core" "^7.26.10" "@babel/plugin-transform-react-jsx-self" "^7.25.9" "@babel/plugin-transform-react-jsx-source" "^7.25.9" + "@rolldown/pluginutils" "1.0.0-beta.9" "@types/babel__core" "^7.20.5" react-refresh "^0.17.0" @@ -1777,7 +1795,7 @@ detect-libc@^2.0.3: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== -detect-newline@^4.0.0: +detect-newline@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-4.0.1.tgz#fcefdb5713e1fb8cb2839b8b6ee22e6716ab8f23" integrity sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog== @@ -2038,19 +2056,19 @@ eslint-visitor-keys@^4.2.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== -eslint@^9.25.1: - version "9.25.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.25.1.tgz#8a7cf8dd0e6acb858f86029720adb1785ee57580" - integrity sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ== +eslint@^9.27.0: + version "9.27.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.27.0.tgz#a587d3cd5b844b68df7898944323a702afe38979" + integrity sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.12.1" "@eslint/config-array" "^0.20.0" "@eslint/config-helpers" "^0.2.1" - "@eslint/core" "^0.13.0" + "@eslint/core" "^0.14.0" "@eslint/eslintrc" "^3.3.1" - "@eslint/js" "9.25.1" - "@eslint/plugin-kit" "^0.2.8" + "@eslint/js" "9.27.0" + "@eslint/plugin-kit" "^0.3.1" "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" "@humanwhocodes/retry" "^0.4.2" @@ -2112,10 +2130,10 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -exifreader@^4.30.0: - version "4.30.0" - resolved "https://registry.yarnpkg.com/exifreader/-/exifreader-4.30.0.tgz#96220f0a1c2e13b30bf3026fdbc0e090687e2433" - integrity sha512-de0tdPkASObswrGtn9fxroP3MiTgDxVu6/wAPQ7cBEdLwgek82s+ox7QT7Sl3ufKM5U1CapN9LWkQfDAKM37Bg== +exifreader@^4.30.1: + version "4.30.1" + resolved "https://registry.yarnpkg.com/exifreader/-/exifreader-4.30.1.tgz#4480d073bce4079e154a57d07cb0c46051d264e5" + integrity sha512-XoEKKQ0FmJwCKHnuErceFAM+MSfZ+px7Nci5BhBP1cgEHi/fHSBvQySsdfd0MaFHzNh8ITsRNwpnvkMuIPicrg== optionalDependencies: "@xmldom/xmldom" "^0.9.4" @@ -2157,11 +2175,6 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -fdir@^6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689" - integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ== - fdir@^6.4.4: version "6.4.4" resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9" @@ -2319,11 +2332,6 @@ get-proto@^1.0.0, get-proto@^1.0.1: dunder-proto "^1.0.1" es-object-atoms "^1.0.0" -get-stdin@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-9.0.0.tgz#3983ff82e03d56f1b2ea0d3e60325f39d703a575" - integrity sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA== - get-symbol-description@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" @@ -2333,17 +2341,17 @@ get-symbol-description@^1.1.0: es-errors "^1.3.0" get-intrinsic "^1.2.6" -get-user-locale@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/get-user-locale/-/get-user-locale-2.3.2.tgz#d37ae6e670c2b57d23a96fb4d91e04b2059d52cf" - integrity sha512-O2GWvQkhnbDoWFUJfaBlDIKUEdND8ATpBXD6KXcbhxlfktyD/d8w6mkzM/IlQEqGZAMz/PW6j6Hv53BiigKLUQ== +get-user-locale@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-user-locale/-/get-user-locale-3.0.0.tgz#3ed8fa51bb8c2225b362168c8db19bc0f6cfc25d" + integrity sha512-iJfHSmdYV39UUBw7Jq6GJzeJxUr4U+S03qdhVuDsR9gCEnfbqLy9gYDJFBJQL1riqolFUKQvx36mEkp2iGgJ3g== dependencies: - mem "^8.0.0" + memoize "^10.0.0" -git-hooks-list@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-3.1.0.tgz#386dc531dcc17474cf094743ff30987a3d3e70fc" - integrity sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA== +git-hooks-list@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-4.1.1.tgz#ae340b82a9312354c73b48007f33840bbd83d3c0" + integrity sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA== glob-parent@^5.1.2: version "5.1.2" @@ -2481,28 +2489,33 @@ i18next-resources-to-backend@^1.2.1: dependencies: "@babel/runtime" "^7.23.2" -i18next@^24.2.3: - version "24.2.3" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-24.2.3.tgz#3a05f72615cbd7c00d7e348667e2aabef1df753b" - integrity sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A== +i18next@^25.2.0: + version "25.2.0" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-25.2.0.tgz#9758164e64abfb9166fca70cba11347d50231ca8" + integrity sha512-ERhJICsxkw1vE7G0lhCUYv4ZxdBEs03qblt1myJs94rYRK9loJF3xDj8mgQz3LmCyp0yYrNjbN/1/GWZTZDGCA== dependencies: - "@babel/runtime" "^7.26.10" + "@babel/runtime" "^7.27.1" -idb@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/idb/-/idb-8.0.2.tgz#349af3974281879889e0572bbb231f978b9f3cf0" - integrity sha512-CX70rYhx7GDDQzwwQMDwF6kDRQi5vVs6khHUumDrMecBylKkwvZ8HWvKV08AGb7VbpoGCWUQ4aHzNDgoUiOIUg== +idb@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/idb/-/idb-8.0.3.tgz#c91e558f15a8d53f1d7f53a094d226fc3ad71fd9" + integrity sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg== ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.2.0, ignore@^5.3.1: +ignore@^5.2.0: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== +ignore@^7.0.0: + version "7.0.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.4.tgz#a12c70d0f2607c5bf508fb65a40c75f037d7a078" + integrity sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A== + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -2939,22 +2952,15 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -map-age-cleaner@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.0" - math-intrinsics@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== -media-chrome@^4.9.1: - version "4.9.1" - resolved "https://registry.yarnpkg.com/media-chrome/-/media-chrome-4.9.1.tgz#57986ecd167e0600a19e4e88823151972d60e284" - integrity sha512-lOdYhWVCYg0Az2VnVKPoE/eUflVrNyauHg8cLa/ibsBg4CB6RhRQBTdQsq36NuiSNSQU0B3vtmaTNFTUFT12RA== +media-chrome@^4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/media-chrome/-/media-chrome-4.10.0.tgz#9d79222e40ab09db1b013d13889de0150ad22d4b" + integrity sha512-XA37xpQaI3IqTpBj8C46RD3DHkEl/KMKrIfNZYQBSm7JTRfsRQKluDGD8nCQ5ZkZdovmI4zSkY9mSjVEN9qsJw== dependencies: "@vercel/edge" "^1.2.1" ce-la-react "^0.1.3" @@ -2964,14 +2970,6 @@ media-tracks@^0.3.3: resolved "https://registry.yarnpkg.com/media-tracks/-/media-tracks-0.3.3.tgz#cca72bd791dcb76cd6427dfa6b2baa25601ea192" integrity sha512-9P2FuUHnZZ3iji+2RQk7Zkh5AmZTnOG5fODACnjhCVveX1McY3jmCRHofIEI+yTBqplz7LXy48c7fQ3Uigp88w== -mem@^8.0.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/mem/-/mem-8.1.1.tgz#cf118b357c65ab7b7e0817bdf00c8062297c0122" - integrity sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA== - dependencies: - map-age-cleaner "^0.1.3" - mimic-fn "^3.1.0" - "memoize-one@>=3.1.1 <6": version "5.2.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" @@ -2982,6 +2980,13 @@ memoize-one@^6.0.0: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== +memoize@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/memoize/-/memoize-10.1.0.tgz#32a9d09da985a1ab518dfe9fd52d14d1d130446f" + integrity sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg== + dependencies: + mimic-function "^5.0.1" + merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -3007,10 +3012,10 @@ mime-types@^2.1.12: dependencies: mime-db "1.52.0" -mimic-fn@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" - integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== +mimic-function@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076" + integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== minimatch@^3.1.2: version "3.1.2" @@ -3077,12 +3082,12 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -next@^15.3.1: - version "15.3.1" - resolved "https://registry.yarnpkg.com/next/-/next-15.3.1.tgz#69cf2c124e504db64e14fc75eb29bd64c0c787a7" - integrity sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g== +next@^15.3.2: + version "15.3.2" + resolved "https://registry.yarnpkg.com/next/-/next-15.3.2.tgz#97510629e38a058dd154782a5c2ec9c9ab94d0d8" + integrity sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ== dependencies: - "@next/env" "15.3.1" + "@next/env" "15.3.2" "@swc/counter" "0.1.3" "@swc/helpers" "0.5.15" busboy "1.6.0" @@ -3090,14 +3095,14 @@ next@^15.3.1: postcss "8.4.31" styled-jsx "5.1.6" optionalDependencies: - "@next/swc-darwin-arm64" "15.3.1" - "@next/swc-darwin-x64" "15.3.1" - "@next/swc-linux-arm64-gnu" "15.3.1" - "@next/swc-linux-arm64-musl" "15.3.1" - "@next/swc-linux-x64-gnu" "15.3.1" - "@next/swc-linux-x64-musl" "15.3.1" - "@next/swc-win32-arm64-msvc" "15.3.1" - "@next/swc-win32-x64-msvc" "15.3.1" + "@next/swc-darwin-arm64" "15.3.2" + "@next/swc-darwin-x64" "15.3.2" + "@next/swc-linux-arm64-gnu" "15.3.2" + "@next/swc-linux-arm64-musl" "15.3.2" + "@next/swc-linux-x64-gnu" "15.3.2" + "@next/swc-linux-x64-musl" "15.3.2" + "@next/swc-win32-arm64-msvc" "15.3.2" + "@next/swc-win32-x64-msvc" "15.3.2" sharp "^0.34.1" node-releases@^2.0.19: @@ -3195,11 +3200,6 @@ p-debounce@^4.0.0: resolved "https://registry.yarnpkg.com/p-debounce/-/p-debounce-4.0.0.tgz#348e3f44489baa9435cc7d807f17b3bb2fb16b24" integrity sha512-4Ispi9I9qYGO4lueiLDhe4q4iK5ERK8reLsuzH6BPaXn53EGaua8H66PXIFGrW897hwjXp+pVLrm/DLxN0RF0A== -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw== - p-limit@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" @@ -3319,13 +3319,13 @@ prettier-plugin-organize-imports@^4.1.0: resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz#f3d3764046a8e7ba6491431158b9be6ffd83b90f" integrity sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A== -prettier-plugin-packagejson@^2.5.10: - version "2.5.10" - resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.10.tgz#f47068d0aa12efcdddb802189d8adae874ba00e7" - integrity sha512-LUxATI5YsImIVSaaLJlJ3aE6wTD+nvots18U3GuQMJpUyClChaZlQrqx3dBnbhF20OnKWZyx8EgyZypQtBDtgQ== +prettier-plugin-packagejson@^2.5.14: + version "2.5.14" + resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.14.tgz#8ada09114ff60c7f42c3f8755ffb2f8152f3624f" + integrity sha512-h+3tSpr2nVpp+YOK1MDIYtYhHVXr8/0V59UUbJpIJFaqi3w4fvUokJo6eV8W+vELrUXIZzJ+DKm5G7lYzrMcKQ== dependencies: - sort-package-json "2.15.1" - synckit "0.9.2" + sort-package-json "3.2.1" + synckit "0.11.6" prettier@^3.5.3: version "3.5.3" @@ -3387,10 +3387,10 @@ react-fast-compare@^2.0.1: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== -react-i18next@^15.4.1: - version "15.4.1" - resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.4.1.tgz#33f3e89c2f6c68e2bfcbf9aa59986ad42fe78758" - integrity sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw== +react-i18next@^15.5.2: + version "15.5.2" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.5.2.tgz#2cfbd8e055efea077a7cbd7fbd9528c76d31925e" + integrity sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A== dependencies: "@babel/runtime" "^7.25.0" html-parse-stringify "^3.0.1" @@ -3649,11 +3649,6 @@ sanitize-filename@^1.6.3: dependencies: truncate-utf8-bytes "^1.0.0" -sax@^1.2.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" - integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== - scheduler@^0.26.0: version "0.26.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337" @@ -3814,19 +3809,18 @@ sort-object-keys@^1.1.3: resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45" integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg== -sort-package-json@2.15.1: - version "2.15.1" - resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.15.1.tgz#e5a035fad7da277b1947b9eecc93ea09c1c2526e" - integrity sha512-9x9+o8krTT2saA9liI4BljNjwAbvUnWf11Wq+i/iZt8nl2UGYnf3TH5uBydE7VALmP7AGwlfszuEeL8BDyb0YA== +sort-package-json@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-3.2.1.tgz#889f3bdf43ceeff5fa4278a7c53ae5b1520d287e" + integrity sha512-rTfRdb20vuoAn7LDlEtCqOkYfl2X+Qze6cLbNOzcDpbmKEhJI30tTN44d5shbKJnXsvz24QQhlCm81Bag7EOKg== dependencies: detect-indent "^7.0.1" - detect-newline "^4.0.0" - get-stdin "^9.0.0" - git-hooks-list "^3.0.0" + detect-newline "^4.0.1" + git-hooks-list "^4.0.0" is-plain-obj "^4.1.0" - semver "^7.6.0" + semver "^7.7.1" sort-object-keys "^1.1.3" - tinyglobby "^0.2.9" + tinyglobby "^0.2.12" source-map-js@^1.0.2, source-map-js@^1.2.1: version "1.2.1" @@ -3976,13 +3970,12 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -synckit@0.9.2: - version "0.9.2" - resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.2.tgz#a3a935eca7922d48b9e7d6c61822ee6c3ae4ec62" - integrity sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw== +synckit@0.11.6: + version "0.11.6" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.6.tgz#e742a0c27bbc1fbc96f2010770521015cca7ed5c" + integrity sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw== dependencies: - "@pkgr/core" "^0.1.0" - tslib "^2.6.2" + "@pkgr/core" "^0.2.4" tiny-case@^1.0.3: version "1.0.3" @@ -3994,7 +3987,7 @@ tiny-warning@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== -tinyglobby@^0.2.13: +tinyglobby@^0.2.12, tinyglobby@^0.2.13: version "0.2.13" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.13.tgz#a0e46515ce6cbcd65331537e57484af5a7b2ff7e" integrity sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw== @@ -4002,14 +3995,6 @@ tinyglobby@^0.2.13: fdir "^6.4.4" picomatch "^4.0.2" -tinyglobby@^0.2.9: - version "0.2.10" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.10.tgz#e712cf2dc9b95a1f5c5bbd159720e15833977a0f" - integrity sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew== - dependencies: - fdir "^6.4.2" - picomatch "^4.0.2" - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -4042,12 +4027,12 @@ truncate-utf8-bytes@^1.0.0: dependencies: utf8-byte-length "^1.0.1" -ts-api-utils@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.0.1.tgz#660729385b625b939aaa58054f45c058f33f10cd" - integrity sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w== +ts-api-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" + integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== -tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.6.2, tslib@^2.8.0: +tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.8.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -4109,14 +4094,14 @@ typed-array-length@^1.0.7: possible-typed-array-names "^1.0.0" reflect.getprototypeof "^1.0.6" -typescript-eslint@^8.31.1: - version "8.31.1" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.31.1.tgz#b77ab1e48ced2daab9225ff94bab54391a4af69b" - integrity sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA== +typescript-eslint@^8.32.1: + version "8.32.1" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.32.1.tgz#1784335c781491be528ff84ab666e2f0f7591fd1" + integrity sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg== dependencies: - "@typescript-eslint/eslint-plugin" "8.31.1" - "@typescript-eslint/parser" "8.31.1" - "@typescript-eslint/utils" "8.31.1" + "@typescript-eslint/eslint-plugin" "8.32.1" + "@typescript-eslint/parser" "8.32.1" + "@typescript-eslint/utils" "8.32.1" typescript@^5.8.3: version "5.8.3" @@ -4173,10 +4158,10 @@ uuid@^11.1.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== -vite@^6.3.4: - version "6.3.4" - resolved "https://registry.yarnpkg.com/vite/-/vite-6.3.4.tgz#d441a72c7cd9a93b719bb851250a4e6c119c9cff" - integrity sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw== +vite@^6.3.5: + version "6.3.5" + resolved "https://registry.yarnpkg.com/vite/-/vite-6.3.5.tgz#fec73879013c9c0128c8d284504c6d19410d12a3" + integrity sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ== dependencies: esbuild "^0.25.0" fdir "^6.4.4" @@ -4265,13 +4250,6 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" -xml-js@^1.6.11: - version "1.6.11" - resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" - integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g== - dependencies: - sax "^1.2.4" - y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" @@ -4320,10 +4298,10 @@ yup@^1.6.1: toposort "^2.0.2" type-fest "^2.19.0" -zod@^3.24.3: - version "3.24.3" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.3.tgz#1f40f750a05e477396da64438e0e1c0995dafd87" - integrity sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg== +zod@^3.25.23: + version "3.25.23" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.23.tgz#128fb02f3619a8bca6bbbf6b07b457236cf33391" + integrity sha512-Od2bdMosahjSrSgJtakrwjMDb1zM1A3VIHCPGveZt/3/wlrTWBya2lmEh2OYe4OIu8mPTmmr0gnLHIWQXdtWBg== zxcvbn@^4.4.2: version "4.4.2"